--- /dev/null
+# See: https://editorconfig.org/
+root = true
+
+[*.{js,json,md,ts}]
+indent_style = space
+indent_size = 4
+
+[portool]
+indent_style = space
+indent_size = 4
bracketSpacing: false
singleQuote: true
+tabWidth: 4
## 2019-10-13
-- [Vim]: Switched from [tpope/vim-markdown](https://github.com/tpope/vim-markdown) to [plasticboy/vim-markdown](https://github.com/plasticboy/vim-markdown); note that because this is replacing one submodule with another of the same name you may need to remove the old submodule directory with `rm` as well as removing the corresponding config in your local `.git/config` in order for the next `git submodule update --init` to succeed.
+- [Vim]: Switched from [tpope/vim-markdown](https://github.com/tpope/vim-markdown) to [plasticboy/vim-markdown](https://github.com/plasticboy/vim-markdown); note that because this is replacing one submodule with another of the same name you may need to remove the old submodule directory with `rm` as well as removing the corresponding config in your local `.git/config` in order for the next `git submodule update --init` to succeed.
## 2019-10-02
-- [Vim]: Add `:OpenOnGitHub` command.
+- [Vim]: Add `:OpenOnGitHub` command.
## 2019-09-27
-- [Vim]: Add `:Typecheck` command.
+- [Vim]: Add `:Typecheck` command.
## 2019-09-24
-- [Vim]: Add `:Lint` command.
+- [Vim]: Add `:Lint` command.
## 2019-09-19
-- [Zsh]: Make tabs visible in `git-diff` output.
+- [Zsh]: Make tabs visible in `git-diff` output.
## 2019-09-14
-- [Vim]: Implement "smart tab" functionality (ie. in projects that use tabs, use tabs for indentation and spacing for alignment).
+- [Vim]: Implement "smart tab" functionality (ie. in projects that use tabs, use tabs for indentation and spacing for alignment).
## 2019-09-14
-- [Zsh]: Add `git cp` alias for `git cherry-pick`.
+- [Zsh]: Add `git cp` alias for `git cherry-pick`.
## 2019-08-23
-- [Zsh]: Add `git wd` alias for `git diff --dirstat` (mnemonic: "[w]hat(changed) [d]irectory").
+- [Zsh]: Add `git wd` alias for `git diff --dirstat` (mnemonic: "[w]hat(changed) [d]irectory").
## 2019-08-22
-- [Vim]: Use floating preview window for LSP "hover" functionality.
+- [Vim]: Use floating preview window for LSP "hover" functionality.
## 2019-07-21
-- [Vim]: Switch from javascript-typescript-langserver to typescript-language-server.
+- [Vim]: Switch from javascript-typescript-langserver to typescript-language-server.
## 2019-06-26
-- [Zsh]: Add `git get` alias equivalent to a `git fresh` (see below) followed by a `git merge`.
+- [Zsh]: Add `git get` alias equivalent to a `git fresh` (see below) followed by a `git merge`.
## 2019-06-17
-- [Zsh]: Add `subtree` function (eg. `subtree '*.js'`, or `subtree '*.js|*.ts' src`).
+- [Zsh]: Add `subtree` function (eg. `subtree '*.js'`, or `subtree '*.js|*.ts' src`).
## 2019-06-07
-- [Vim]: Add `table` snippet for use in Markdown files.
+- [Vim]: Add `table` snippet for use in Markdown files.
## 2019-05-23
-- [Vim]: Moved plug-in submodules into "[~/.vim/pack/bundle/opt](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack/bundle/opt)"
+- [Vim]: Moved plug-in submodules into "[~/.vim/pack/bundle/opt](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack/bundle/opt)"
## 2019-04-26
-- [Zsh]: Add `git fresh` alias for `git remote update --prune`.
+- [Zsh]: Add `git fresh` alias for `git remote update --prune`.
## 2019-03-29
-- [Zsh]: Make Shift-Tab go to previous completion.
-- [Vim]: Force use of NERDTree instead of netrw when opening Vim with a directory argument.
+- [Zsh]: Make Shift-Tab go to previous completion.
+- [Vim]: Force use of NERDTree instead of netrw when opening Vim with a directory argument.
## 2019-03-27
-- [Vim]: Improve set-up for doing merge conflict resolution.
+- [Vim]: Improve set-up for doing merge conflict resolution.
## 2019-03-08
-- [Zsh]: Add `git wc` alias for `git whatchanged`.
+- [Zsh]: Add `git wc` alias for `git whatchanged`.
## 2019-02-27
-- [Zsh]: Add `git message` (shortcut: `git msg`) alias.
+- [Zsh]: Add `git message` (shortcut: `git msg`) alias.
## 2019-02-15
-- [Vim]: Make deoplete rank file-path matches above others.
+- [Vim]: Make deoplete rank file-path matches above others.
## 2019-02-14
-- [Vim]: `<S-Up>`, `<S-Down>` etc can be used to navigate with location list results.
+- [Vim]: `<S-Up>`, `<S-Down>` etc can be used to navigate with location list results.
## 2019-02-07
-- [Vim]: Apply tweaks for working in repos that use tabs instead of spaces.
+- [Vim]: Apply tweaks for working in repos that use tabs instead of spaces.
## 2019-01-25
-- [Vim]: Make improvements for working with TypeScript.
+- [Vim]: Make improvements for working with TypeScript.
## 2019-01-23
-- [Zsh]: Add `git ff` alias for doing fast-forward merges.
+- [Zsh]: Add `git ff` alias for doing fast-forward merges.
## 2019-01-10
-- [Vim]: Bind `<C-minus>` to `:NERDTreeFind`, to complement the existing `-` binding that does in-place directory navigation (vim-vinegar style).
+- [Vim]: Bind `<C-minus>` to `:NERDTreeFind`, to complement the existing `-` binding that does in-place directory navigation (vim-vinegar style).
## 2019-01-03
-- [Vim]: `J` and `K` visual mode mappings now take a count.
+- [Vim]: `J` and `K` visual mode mappings now take a count.
## 2018-12-24
-- [Zsh]: Running `color` without arguments refreshes the color scheme in the current window.
+- [Zsh]: Running `color` without arguments refreshes the color scheme in the current window.
## 2018-12-18
-- [Vim,Zsh]: Switched default color scheme to "default-dark".
+- [Vim,Zsh]: Switched default color scheme to "default-dark".
## 2018-11-25
-- [Zsh]: Overhaul prompt.
+- [Zsh]: Overhaul prompt.
## 2018-10-20
-- [Vim]: Set up [Goyo](https://github.com/junegunn/goyo.vim).
+- [Vim]: Set up [Goyo](https://github.com/junegunn/goyo.vim).
## 2018-03-21
-- Set up "fn" and "power" key equivalents on external keyboard.
+- Set up "fn" and "power" key equivalents on external keyboard.
## 2018-03-08
-- [Zsh]: Add `tw` and `tick` shell utilities.
+- [Zsh]: Add `tw` and `tick` shell utilities.
## 2018-03-06
-- [Vim]: Prevent `'spelllang'` setting from getting blown away in Markdown files.
+- [Vim]: Prevent `'spelllang'` setting from getting blown away in Markdown files.
## 2017-12-28
-- Make Caps Lock and Return repeat when held down.
-- Add back SpaceFN layer.
-- [Vim]: Add `<LocalLeader>p` mapping to print the highlight groups that apply at the current cursor position.
+- Make Caps Lock and Return repeat when held down.
+- Add back SpaceFN layer.
+- [Vim]: Add `<LocalLeader>p` mapping to print the highlight groups that apply at the current cursor position.
## 2017-12-22
-- [Vim]: Add "J"/"K" bindings to move visual selection up and down.
-- [Vim]: Add convenience `wincent#debug#log()` function for debugging purposes.
+- [Vim]: Add "J"/"K" bindings to move visual selection up and down.
+- [Vim]: Add convenience `wincent#debug#log()` function for debugging purposes.
## 2017-12-15
-- [Mutt]: Add "O" macro to save original message (mnemonic: "[O]riginal").
-- [Mutt]: Add "S" macro to save all attachments (mnemonic: "[S]ave").
+- [Mutt]: Add "O" macro to save original message (mnemonic: "[O]riginal").
+- [Mutt]: Add "S" macro to save all attachments (mnemonic: "[S]ave").
## 2017-12-06
-- Use iTerm dynamic profiles to change the font size when an external monitor is present.
+- Use iTerm dynamic profiles to change the font size when an external monitor is present.
## 2017-11-08
-- [Vim]: Dump YouCompleteMe.
+- [Vim]: Dump YouCompleteMe.
## 2017-11-01
-- [Mutt]: Mutt now uses different configs for work and personal machines.
+- [Mutt]: Mutt now uses different configs for work and personal machines.
## 2017-10-20
-- [Zsh]: Add `fzf`-powered functions for finding directories and history entries.
+- [Zsh]: Add `fzf`-powered functions for finding directories and history entries.
## 2017-06-16
-- Switch to Karabiner-Elements.
+- Switch to Karabiner-Elements.
## 2017-06-14
-- [Zsh]: Start using zsh-autosuggestions plug-in.
+- [Zsh]: Start using zsh-autosuggestions plug-in.
## 2017-06-06
-- [Vim]: Neovim is now the default `$EDITOR`.
+- [Vim]: Neovim is now the default `$EDITOR`.
## 2017-05-03
-- [Zsh]: Prompt now shows `$SHLVL` by repeating the `$` or `#` symbol.
-- [Zsh]: Prompt now indicates the presence of background jobs with a `*`.
-- [Vim]: Now turns off syntax highlighting in inactive splits.
+- [Zsh]: Prompt now shows `$SHLVL` by repeating the `$` or `#` symbol.
+- [Zsh]: Prompt now indicates the presence of background jobs with a `*`.
+- [Vim]: Now turns off syntax highlighting in inactive splits.
## 2017-02-09
-- [Vim]: Use `par` to re-wrap text.
+- [Vim]: Use `par` to re-wrap text.
## 2017-01-19
-- Get emoji working in the pager.
+- Get emoji working in the pager.
## 2016-12-24
-- [Mutt]: Use Markdown to send HTML email.
+- [Mutt]: Use Markdown to send HTML email.
## 2016-12-16
-- [Mutt]: Add retry with exponential backoff to mail sync script.
+- [Mutt]: Add retry with exponential backoff to mail sync script.
## 2016-12-14
-- Colorize man pages.
+- Colorize man pages.
## 2016-12-13
-- [Mutt]: Switched from `offlineimap` to `mbsync` (in the `isync` package) for mail synchronization.
+- [Mutt]: Switched from `offlineimap` to `mbsync` (in the `isync` package) for mail synchronization.
## 2016-12-12
-- [Mutt]: Added address autocompletion (via custom YouCompleteMe completer) inside Vim buffers of with filetype "mail".
+- [Mutt]: Added address autocompletion (via custom YouCompleteMe completer) inside Vim buffers of with filetype "mail".
## 2016-12-11
-- [Mutt]: Switched from `contacts` to `lbdb` for searching contacts.
+- [Mutt]: Switched from `contacts` to `lbdb` for searching contacts.
## 2016-12-07
-- [Mutt]: Switched from `w3m` to `elinks` for viewing links within emails.
+- [Mutt]: Switched from `w3m` to `elinks` for viewing links within emails.
## 2016-12-02
-- [Mutt]: Added `mutt` config.
+- [Mutt]: Added `mutt` config.
## 2016-11-30
-- [Vim]: Fine-tuned startup performance from 500ms down to 150ms.
+- [Vim]: Fine-tuned startup performance from 500ms down to 150ms.
## 2016-11-29
-- [Vim,Zsh]: Updated base16 dependencies, which means that the existing `dark`/`light` scheme names no longer apply. Instead of `dark tomorrow` (`color dark tomorrow`) or `light tomorrow` (`color light tomorrow`), run `color tomorrow-night` and `color tomorrow`. Note that some schemes [no longer have light variants](https://github.com/chriskempson/base16/issues/42) at all. `color` continues to show currently configured scheme information and `color help` shows a list of all available colors.
+- [Vim,Zsh]: Updated base16 dependencies, which means that the existing `dark`/`light` scheme names no longer apply. Instead of `dark tomorrow` (`color dark tomorrow`) or `light tomorrow` (`color light tomorrow`), run `color tomorrow-night` and `color tomorrow`. Note that some schemes [no longer have light variants](https://github.com/chriskempson/base16/issues/42) at all. `color` continues to show currently configured scheme information and `color help` shows a list of all available colors.
## 2016-11-28
-- Removed BSD license and replaced with public domain dedication.
+- Removed BSD license and replaced with public domain dedication.
## 2016-11-22
-- Replaced Karabiner configuration with custom Hammerspoon configuration, because Karabiner does not work on macOS Sierra.
- - Features that survived translation:
- - `<Capslock>` and `<Return>` retain their dual-purpose functionalities.
- - `<Tab>` and `<C-i>` can still be mapped independently in the terminal.
- - Features that have not yet been ported:
- - "SpaceFN" layer.
- - Features unlikely to be ported due to technical constraints:
- - `<Shift>` control over Caps Lock state.
- - Features that will not be ported because they can be solved by other means:
- - Remapping of YubiKey to work with Colemak.
+- Replaced Karabiner configuration with custom Hammerspoon configuration, because Karabiner does not work on macOS Sierra.
+ - Features that survived translation:
+ - `<Capslock>` and `<Return>` retain their dual-purpose functionalities.
+ - `<Tab>` and `<C-i>` can still be mapped independently in the terminal.
+ - Features that have not yet been ported:
+ - "SpaceFN" layer.
+ - Features unlikely to be ported due to technical constraints:
+ - `<Shift>` control over Caps Lock state.
+ - Features that will not be ported because they can be solved by other means:
+ - Remapping of YubiKey to work with Colemak.
## 2016-11-14
-- [Vim]: Add "breakpoints" to statusline that reduce the amount of information displayed as window width decreases.
-- [Vim]: Extracted macro replay functionality into a separate plug-in, [Replay](https://github.com/wincent/replay).
+- [Vim]: Add "breakpoints" to statusline that reduce the amount of information displayed as window width decreases.
+- [Vim]: Extracted macro replay functionality into a separate plug-in, [Replay](https://github.com/wincent/replay).
## 2016-11-10
-- [tmux]: Adding `<Prefix>-b` binding to jump back to previous shell prompt.
+- [tmux]: Adding `<Prefix>-b` binding to jump back to previous shell prompt.
## 2016-10-12
-- [tmux]: Change color of active/inactive panes to make currently active pane more obvious.
+- [tmux]: Change color of active/inactive panes to make currently active pane more obvious.
## 2016-10-11
-- Manage most of the macOS preferences via the Ansible `osx_defaults` module, instead of custom Ansible `command` tasks.
+- Manage most of the macOS preferences via the Ansible `osx_defaults` module, instead of custom Ansible `command` tasks.
## 2016-10-07
-- [Zsh]: Make `C-z` run `fg` at the shell prompt.
+- [Zsh]: Make `C-z` run `fg` at the shell prompt.
## 2016-07-05
-- [Zsh]: Bounce Dock icon when a shell command finishes running and the terminal is in the background.
+- [Zsh]: Bounce Dock icon when a shell command finishes running and the terminal is in the background.
## 2016-06-03
-- Set up [Clipper](https://github.com/wincent/clipper) to work via UNIX domain sockets rather than TCP ports for better security.
+- Set up [Clipper](https://github.com/wincent/clipper) to work via UNIX domain sockets rather than TCP ports for better security.
## 2016-05-11
-- [Vim,Zsh]: Switched default color scheme to "tomorrow-dark" (later renamed to "tomorrow-night").
+- [Vim,Zsh]: Switched default color scheme to "tomorrow-dark" (later renamed to "tomorrow-night").
## 2016-05-09
-- [Vim]: Use `<Tab>` to toggle folds.
+- [Vim]: Use `<Tab>` to toggle folds.
## 2016-05-02
-- Work around lengthy hangs running Ansible on macOS.
+- Work around lengthy hangs running Ansible on macOS.
## 2016-04-29
-- [Vim]: Extracted within-file find-and-replace enhancements into a separate plug-in, [Scalpel](https://github.com/wincent/scalpel).
+- [Vim]: Extracted within-file find-and-replace enhancements into a separate plug-in, [Scalpel](https://github.com/wincent/scalpel).
## 2016-04-10
-- [Vim]: Use "Powerline" glyphs to make statusline a little prettier.
-- Switch to Adobe Source Code Pro font.
+- [Vim]: Use "Powerline" glyphs to make statusline a little prettier.
+- Switch to Adobe Source Code Pro font.

-- Target platforms: macOS and Red Hat-like Linuxes (eg. CentOS).
-- Set-up method: ~~Beautiful and intricate snowflake~~ incredibly over-engineered [Ansible](https://www.ansible.com/) orchestration.
-- Visible in the screenshot:
- - [default-dark Base16](http://chriskempson.com/projects/base16/) color scheme.
- - [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) (Light) font.
- - Vim, running inside tmux, inside iTerm2, on macOS "High Sierra".
+- Target platforms: macOS and Red Hat-like Linuxes (eg. CentOS).
+- Set-up method: ~~Beautiful and intricate snowflake~~ incredibly over-engineered [Ansible](https://www.ansible.com/) orchestration.
+- Visible in the screenshot:
+ - [default-dark Base16](http://chriskempson.com/projects/base16/) color scheme.
+ - [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) (Light) font.
+ - Vim, running inside tmux, inside iTerm2, on macOS "High Sierra".
## Features
[A set of dotfiles](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files) that I've been tweaking and twiddling since the early 2000s ([under version control](https://github.com/wincent/wincent/commit/61a7e2a830edb7) since 2009). Characteristics include:
-- Sane Vim pasting via bracketed paste mode.
-- Write access to local clipboard from local and remote hosts, inside and outside of tmux (via [Clipper](https://github.com/wincent/clipper)).
-- Full mouse support (pane/split resizing, scrolling, text selection) in Vim and tmux.
-- Focus/lost events for Vim inside tmux.
-- Cursor shape toggles on entering Vim.
-- Italics in the terminal.
-- Bundles a (not-excessive) number of [useful Vim plug-ins](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack).
-- Conservative Vim configuration (very few overrides of core functionality; most changes are unobtrusive enhancements; some additional functionality exposed via `<Leader>` and `<LocalLeader>` mappings.
-- Relatively restrained Zsh config, Bash-like but with a few Zsh perks, such as right-side prompt, auto-cd hooks, command elapsed time printing and such.
-- Unified color-handling (across iTerm2 and Vim) via [Base16 Shell](https://github.com/chriskempson/base16-shell).
-- Encrypted versioning of files with sensitive content (via [git-cipher](https://github.com/wincent/git-cipher)).
-- Comprehensive [Hammerspoon](http://www.hammerspoon.org/) [config](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.hammerspoon).
+- Sane Vim pasting via bracketed paste mode.
+- Write access to local clipboard from local and remote hosts, inside and outside of tmux (via [Clipper](https://github.com/wincent/clipper)).
+- Full mouse support (pane/split resizing, scrolling, text selection) in Vim and tmux.
+- Focus/lost events for Vim inside tmux.
+- Cursor shape toggles on entering Vim.
+- Italics in the terminal.
+- Bundles a (not-excessive) number of [useful Vim plug-ins](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack).
+- Conservative Vim configuration (very few overrides of core functionality; most changes are unobtrusive enhancements; some additional functionality exposed via `<Leader>` and `<LocalLeader>` mappings.
+- Relatively restrained Zsh config, Bash-like but with a few Zsh perks, such as right-side prompt, auto-cd hooks, command elapsed time printing and such.
+- Unified color-handling (across iTerm2 and Vim) via [Base16 Shell](https://github.com/chriskempson/base16-shell).
+- Encrypted versioning of files with sensitive content (via [git-cipher](https://github.com/wincent/git-cipher)).
+- Comprehensive [Hammerspoon](http://www.hammerspoon.org/) [config](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.hammerspoon).
### Homebrew
On macOS, [Karabiner-Elements](https://github.com/tekezo/Karabiner-Elements/) is used for the following:
-- Make Caps Lock serve as Backspace (when tapped) and Left Control (when chorded with another key). When held down alone, Caps Lock fires repeated Backspace events.
-- Make Return serve as Return (when tapped) and Right Control (when chorded with another key). When held down alone, Return fires repeated Return events.
-- Maps Control-I to F6 (only in MacVim and the terminal) so that it can be mapped independently from Tab in Vim.
-- Toggle Caps Lock on by tapping both Shift keys simultaneously.
-- Makes the function keys on my external Realforce keyboard behave like the "media" keys on Apple's keyboards.
-- Swap Option and Command keys on my external Realforce keyboard.
-- Make the "application" key (extra modifier key on right-hand side) behave as "fn" on Realforce keyboard.
-- Make "pause" (at far-right of function key row) behave as "power" (effectively, sleep) on Realforce keyboard.
-- Adds a "SpaceFN" layer that can be activated by holding down Space while hitting other keys; I use this to make the cursor keys available on or near the home row in any app.
+- Make Caps Lock serve as Backspace (when tapped) and Left Control (when chorded with another key). When held down alone, Caps Lock fires repeated Backspace events.
+- Make Return serve as Return (when tapped) and Right Control (when chorded with another key). When held down alone, Return fires repeated Return events.
+- Maps Control-I to F6 (only in MacVim and the terminal) so that it can be mapped independently from Tab in Vim.
+- Toggle Caps Lock on by tapping both Shift keys simultaneously.
+- Makes the function keys on my external Realforce keyboard behave like the "media" keys on Apple's keyboards.
+- Swap Option and Command keys on my external Realforce keyboard.
+- Make the "application" key (extra modifier key on right-hand side) behave as "fn" on Realforce keyboard.
+- Make "pause" (at far-right of function key row) behave as "power" (effectively, sleep) on Realforce keyboard.
+- Adds a "SpaceFN" layer that can be activated by holding down Space while hitting other keys; I use this to make the cursor keys available on or near the home row in any app.
### Zsh
#### Functions
-- `ag`: Transparently wraps the `ag` executable so as to provide a centralized place to set defaults for that command (seeing as it has no "rc" file).
-- `bounce`: bounce the macOS Dock icon if the terminal is not in the foreground.
-- `color`: change terminal and Vim color scheme.
-- `email`: convenience wrapper to spawn (or attach to) a tmux session running `mutt` and `mbsync`.
-- `fd`: "find directory" using fast `bfs` and `sk`; automatically `cd`s into the selected directory.
-- `fh`: "find [in] history"; selecting a history item inserts it into the command line but does not execute it.
-- `history`: overrides the (tiny) default history count.
-- `jump` (aliased to `j`): to jump to hashed directories.
-- `regmv`: bulk-rename files (eg. `regmv '/\.tif$/.tiff/' *`).
-- `scratch`: create a random temporary scratch directory and `cd` into it.
-- `tick`: moves an existing time warp (eg. `tick +1h`); see `tw` below for a description of time warp.
-- `tmux`: wrapper that reattches to pre-existing sessions, or creates new ones based on the current directory name; additionally, looks for a `.tmux` file to set up windows and panes (note that the first time a given `.tmux` file is encountered the wrapper asks the user whether to trust or skip it).
-- `tw` ("time warp"): overrides `GIT_AUTHOR_DATE` and `GIT_COMMITTER_DATE` (eg. `tw -1d`).
+- `ag`: Transparently wraps the `ag` executable so as to provide a centralized place to set defaults for that command (seeing as it has no "rc" file).
+- `bounce`: bounce the macOS Dock icon if the terminal is not in the foreground.
+- `color`: change terminal and Vim color scheme.
+- `email`: convenience wrapper to spawn (or attach to) a tmux session running `mutt` and `mbsync`.
+- `fd`: "find directory" using fast `bfs` and `sk`; automatically `cd`s into the selected directory.
+- `fh`: "find [in] history"; selecting a history item inserts it into the command line but does not execute it.
+- `history`: overrides the (tiny) default history count.
+- `jump` (aliased to `j`): to jump to hashed directories.
+- `regmv`: bulk-rename files (eg. `regmv '/\.tif$/.tiff/' *`).
+- `scratch`: create a random temporary scratch directory and `cd` into it.
+- `tick`: moves an existing time warp (eg. `tick +1h`); see `tw` below for a description of time warp.
+- `tmux`: wrapper that reattches to pre-existing sessions, or creates new ones based on the current directory name; additionally, looks for a `.tmux` file to set up windows and panes (note that the first time a given `.tmux` file is encountered the wrapper asks the user whether to trust or skip it).
+- `tw` ("time warp"): overrides `GIT_AUTHOR_DATE` and `GIT_COMMITTER_DATE` (eg. `tw -1d`).
#### Prompt
Visible here are:
-- Concise left-hand prompt consisting of:
- - Last component of current directory (abbreviates `$HOME` to `~` if possible).
- - Prompt marker, `❯`, the "[HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT](https://codepoints.net/U+276F)" (that's `\u276f`, or `e2 9d af` in UTF-8).
-- Extended right-hand size prompt which auto-hides when necessary to make room for long commands and contains:
- - Duration of previous command in adaptive units (seconds, minutes, hours, days, depending on duration).
- - Current version control branch name.
- - Current version control worktree status using colors that match those used in `git status`:
- - Green dot indicates staged changes.
- - Red dot indicates unstaged changes.
- - Blue dot indicates untracked files.
- - Full version of current working directory (again, abbreviating `$HOME` to `~`).
+- Concise left-hand prompt consisting of:
+ - Last component of current directory (abbreviates `$HOME` to `~` if possible).
+ - Prompt marker, `❯`, the "[HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT](https://codepoints.net/U+276F)" (that's `\u276f`, or `e2 9d af` in UTF-8).
+- Extended right-hand size prompt which auto-hides when necessary to make room for long commands and contains:
+ - Duration of previous command in adaptive units (seconds, minutes, hours, days, depending on duration).
+ - Current version control branch name.
+ - Current version control worktree status using colors that match those used in `git status`:
+ - Green dot indicates staged changes.
+ - Red dot indicates unstaged changes.
+ - Blue dot indicates untracked files.
+ - Full version of current working directory (again, abbreviating `$HOME` to `~`).
Nested shells are indicated with additional prompt characters. For example, one nested shell:
A number of tools are used to provide command-line access to Gmail and Office IMAP accounts.
-- [mutt](http://www.mutt.org/): For reading email.
-- [isync](http://isync.sourceforge.net/): For maintaining a local cache of messages for offline access.
-- [notmuch](https://notmuchmail.org/): For fast search.
-- [msmtp](http://msmtp.sourceforge.net/): For sending email.
-- [elinks](http://elinks.or.cz/): For viewing HTML emails.
-- [urlview](https://packages.debian.org/sid/misc/urlview): For opening URLs from inside mutt.
-- [terminal-notifier](https://github.com/julienXX/terminal-notifier): For notifications.
-- [lbdb](https://www.spinnaker.de/lbdb/): Contact autocompletion drawing from a number of sources, such as previous messages, aliases, and macOS Contacts (which can be configured to synchronize Google contacts as well).
-- [imapfilter](https://github.com/lefcha/imapfilter/): For filtering.
-- [passage](https://github.com/wincent/passage): For mediating interaction with the macOS keychain.
+- [mutt](http://www.mutt.org/): For reading email.
+- [isync](http://isync.sourceforge.net/): For maintaining a local cache of messages for offline access.
+- [notmuch](https://notmuchmail.org/): For fast search.
+- [msmtp](http://msmtp.sourceforge.net/): For sending email.
+- [elinks](http://elinks.or.cz/): For viewing HTML emails.
+- [urlview](https://packages.debian.org/sid/misc/urlview): For opening URLs from inside mutt.
+- [terminal-notifier](https://github.com/julienXX/terminal-notifier): For notifications.
+- [lbdb](https://www.spinnaker.de/lbdb/): Contact autocompletion drawing from a number of sources, such as previous messages, aliases, and macOS Contacts (which can be configured to synchronize Google contacts as well).
+- [imapfilter](https://github.com/lefcha/imapfilter/): For filtering.
+- [passage](https://github.com/wincent/passage): For mediating interaction with the macOS keychain.
In order for all this to work, a few items have to be stored in the macOS keychain:
-- A "generic" (A.K.A. "application") keychain items (that is, without protocols, only hostnames):
- - "Keychain Item Name": example.net (corresponds to the "host" field in `~/.msmtprc`, and "Host" field in `~/.mbsyncrc`).
- - "Account Name": username+mutt@example.net (corresponds to the "user" field in `~/.msmtprc`, and "PassCmd" field in `~/.mbsynrc`).
+- A "generic" (A.K.A. "application") keychain items (that is, without protocols, only hostnames):
+ - "Keychain Item Name": example.net (corresponds to the "host" field in `~/.msmtprc`, and "Host" field in `~/.mbsyncrc`).
+ - "Account Name": username+mutt@example.net (corresponds to the "user" field in `~/.msmtprc`, and "PassCmd" field in `~/.mbsynrc`).
The following Gmail-like/Vim-like bindings are configured:
-- `e`: Archive (but note: leaves copy of mail in mailbox until next sync; force an immediate sync with `$`).
-- `#`: Trash mail.
-- `!`: Mark as spam.
-- `gi`: Go to inbox.
-- `ga`: Go to archive.
-- `gt`: Go to sent mail.
-- `gd`: Go to drafts.
-- `gs`: Go to starred mail.
-- `gl`: Go to a label (folder).
-- `x`: Toggle selection on entry (see also `t`).
-- `c`: Compose new message.
-- `s`: Toggle star.
-- `*a`: Select all.
-- `*n`: Deselect all (mnemonic: "select none").
-- `*r`: Select read messages.
-- `*u`: Select unread messages.
-- `Shift-U`: Mark as unread.
-- `Shift-I`: Mark as read.
+- `e`: Archive (but note: leaves copy of mail in mailbox until next sync; force an immediate sync with `$`).
+- `#`: Trash mail.
+- `!`: Mark as spam.
+- `gi`: Go to inbox.
+- `ga`: Go to archive.
+- `gt`: Go to sent mail.
+- `gd`: Go to drafts.
+- `gs`: Go to starred mail.
+- `gl`: Go to a label (folder).
+- `x`: Toggle selection on entry (see also `t`).
+- `c`: Compose new message.
+- `s`: Toggle star.
+- `*a`: Select all.
+- `*n`: Deselect all (mnemonic: "select none").
+- `*r`: Select read messages.
+- `*u`: Select unread messages.
+- `Shift-U`: Mark as unread.
+- `Shift-I`: Mark as read.
Standard `mutt` stuff:
-- `v`: View attachments (including alternate parts for a multipart message).
+- `v`: View attachments (including alternate parts for a multipart message).
Non-Gmail extensions:
-- `t`: Toggle selection on entire thread (see also `x`).
-- `A`: Show alternate MIME-type in MIME-multipart messages.
-- `O`: Save original message.
-- `S`: Search all using [Xapian query syntax](https://xapian.org/docs/queryparser.html) ([notmuch-specific reference documentation](https://notmuchmail.org/manpages/notmuch-search-terms-7/)):
- - `+foo`: Must include "foo".
- - `-bar`: Must not include "bar".
- - `AND`, `OR`, `NOT`, `XOR`: Self-evident.
- - `foo NEAR bar`: "foo" within 10 words of "bar" (order-independent).
- - `foo ADJ bar`: Like `NEAR`, but "foo" must appear earlier than "bar".
- - `"foo bar"`: Match entire phrase.
- - `foo*`: Match "foo", "food", "foobar" etc.
- - `subject:this`, `subject:"one two"` (two consecutive words), `subject:(one two)` (either or both words anywhere in subject), `subject:one AND subject:two` (both words anywhere in subject).
- - `subject:/regex.*/` (but note, quotes are needed for patterns containing spaces; eg. `subject:"/a b/"`).
- - `from:john`, `from:me@example.com`
- - `to:john`, `to:me@example.com`
- - `date:today`
- - `date:yesterday`
- - `date:3d` (exactly 3 days ago)
- - `date:14d..7d` (a week ago)
- - `date:10d..` (since 10 days ago)
- - `date:..3d` (until 3 days ago)
- - `date:"last week"` (preceding Monday through Sunday)
- - `date:"this week"` or `date:this_week` or `date:this-week` (Monday to present day)
- - `date:"last year"` (also works with `years`, `months`, `hours`/`hrs`, `minutes`/`mins`, `seconds`/`secs` etc).
- - `date:june`
- - `date:2018-06-01`
- - `is:{tag}`: eg. `is:unread`, `is:flagged` (ie. starred); to see all tags, run `notmuch search --output=tags '*'`:
- - `attachment`
- - `flagged`
- - `inbox` (not very meaningful as _everything_ gets this tag when indexed via `notmuch new`)
- - `replied`
- - `signed`
- - `unread`
- - `id:messageId@example.net` (search by Message-Id).
-- `l`: Limit listed messages:
- - `~f bob` (from bob)
- - `~s foo` (subject contains "foo"; "Foo" would search case-sensitively)
- - `~s foo.+bar` (subject contains pattern)
- - `!~s foo` (subject does not contain "foo")
- - `~d >1m` (messages more than 1 month old)
-- `\u`: Open list of URLs in message (via `urlview`).
-- `b`: Toggle (mailboxes) sidebar.
-- `m`: Move message(s).
+- `t`: Toggle selection on entire thread (see also `x`).
+- `A`: Show alternate MIME-type in MIME-multipart messages.
+- `O`: Save original message.
+- `S`: Search all using [Xapian query syntax](https://xapian.org/docs/queryparser.html) ([notmuch-specific reference documentation](https://notmuchmail.org/manpages/notmuch-search-terms-7/)):
+ - `+foo`: Must include "foo".
+ - `-bar`: Must not include "bar".
+ - `AND`, `OR`, `NOT`, `XOR`: Self-evident.
+ - `foo NEAR bar`: "foo" within 10 words of "bar" (order-independent).
+ - `foo ADJ bar`: Like `NEAR`, but "foo" must appear earlier than "bar".
+ - `"foo bar"`: Match entire phrase.
+ - `foo*`: Match "foo", "food", "foobar" etc.
+ - `subject:this`, `subject:"one two"` (two consecutive words), `subject:(one two)` (either or both words anywhere in subject), `subject:one AND subject:two` (both words anywhere in subject).
+ - `subject:/regex.*/` (but note, quotes are needed for patterns containing spaces; eg. `subject:"/a b/"`).
+ - `from:john`, `from:me@example.com`
+ - `to:john`, `to:me@example.com`
+ - `date:today`
+ - `date:yesterday`
+ - `date:3d` (exactly 3 days ago)
+ - `date:14d..7d` (a week ago)
+ - `date:10d..` (since 10 days ago)
+ - `date:..3d` (until 3 days ago)
+ - `date:"last week"` (preceding Monday through Sunday)
+ - `date:"this week"` or `date:this_week` or `date:this-week` (Monday to present day)
+ - `date:"last year"` (also works with `years`, `months`, `hours`/`hrs`, `minutes`/`mins`, `seconds`/`secs` etc).
+ - `date:june`
+ - `date:2018-06-01`
+ - `is:{tag}`: eg. `is:unread`, `is:flagged` (ie. starred); to see all tags, run `notmuch search --output=tags '*'`:
+ - `attachment`
+ - `flagged`
+ - `inbox` (not very meaningful as _everything_ gets this tag when indexed via `notmuch new`)
+ - `replied`
+ - `signed`
+ - `unread`
+ - `id:messageId@example.net` (search by Message-Id).
+- `l`: Limit listed messages:
+ - `~f bob` (from bob)
+ - `~s foo` (subject contains "foo"; "Foo" would search case-sensitively)
+ - `~s foo.+bar` (subject contains pattern)
+ - `!~s foo` (subject does not contain "foo")
+ - `~d >1m` (messages more than 1 month old)
+- `\u`: Open list of URLs in message (via `urlview`).
+- `b`: Toggle (mailboxes) sidebar.
+- `m`: Move message(s).
Other stuff:
-- `<Tab>` autocompletes addresses from the lbdb database.
-- `<C-t>` autocompletes aliases.
+- `<Tab>` autocompletes addresses from the lbdb database.
+- `<C-t>` autocompletes aliases.
Attachment menu bindings:
-- `S`: Save all attachments.
+- `S`: Save all attachments.
To have `mailto` links open up in `mutt` in iTerm:
Notes:
-- `$$URL$$` is documented [here](https://groups.google.com/d/msg/iterm2-discuss/TFPl1D_miIU/uDVV2ZZpYWQJ).
-- The convoluted use of `env` and `zsh` is required to get terminal colors working correctly.
+- `$$URL$$` is documented [here](https://groups.google.com/d/msg/iterm2-discuss/TFPl1D_miIU/uDVV2ZZpYWQJ).
+- The convoluted use of `env` and `zsh` is required to get terminal colors working correctly.
## Dependencies
-- [tmux](http://tmux.sourceforge.net/) 2.3 or later.
-- [Neovim](https://neovim.io) or [Vim](http://www.vim.org/) 8.0 or later with Ruby and Python support (although there's a reasonable amount of feature detection in order to degrade gracefully).
-- Relatively recent [Zsh](http://www.zsh.org/).
-- Relatively recent [Git](http://git-scm.com/).
-- [Clipper](https://wincent.com/products/clipper) for transparent access to the local system clipboard.
-- On macOS, [iTerm2](http://www.iterm2.com/). Additionally, only the latest version of macOS (although at the time of writing, I'm still on High Sierra) gets actively tested.
-- [Python](https://www.python.org/) to perform setup via the included `install` command.
-- [Ruby](https://www.ruby-lang.org/).
-- [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) or any other fixed-width font that includes the [Powerline glyphs](http://powerline.readthedocs.io/en/master/installation.html#fonts-installation).
+- [tmux](http://tmux.sourceforge.net/) 2.3 or later.
+- [Neovim](https://neovim.io) or [Vim](http://www.vim.org/) 8.0 or later with Ruby and Python support (although there's a reasonable amount of feature detection in order to degrade gracefully).
+- Relatively recent [Zsh](http://www.zsh.org/).
+- Relatively recent [Git](http://git-scm.com/).
+- [Clipper](https://wincent.com/products/clipper) for transparent access to the local system clipboard.
+- On macOS, [iTerm2](http://www.iterm2.com/). Additionally, only the latest version of macOS (although at the time of writing, I'm still on High Sierra) gets actively tested.
+- [Python](https://www.python.org/) to perform setup via the included `install` command.
+- [Ruby](https://www.ruby-lang.org/).
+- [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) or any other fixed-width font that includes the [Powerline glyphs](http://powerline.readthedocs.io/en/master/installation.html#fonts-installation).
## Installation
- name: debugging bad stat info
debug:
- var: stat_result
+ var: stat_result
```
Note that for convenience, "debug" tasks have already been inserted for all variables that are `register`-ed in the existing roles, with verbosity thresholds of 2, meaning that they will be logged automatically when the install is run using `./install -vv` or more.
The goal was to replace Ansible with some handmade scripts using the smallest dependency graph possible. I original [tried](https://github.com/wincent/wincent/commit/8809a1681cfd8fd02eb40113d2485d7cadc10e4c) out [Deno](https://deno.land/) because that would enable me to use TypeScript with no dependencies outside of Deno itself, however I [gave up on that](https://github.com/wincent/wincent/commit/a213ddf69d3213882808b5c5ff0e000bcd83fe98) when I saw that editor integration was still very nascent. So I went with the following:
-- [n](https://github.com/tj/n) ([as a submodule](https://github.com/wincent/wincent/tree/master/vendor)) and some [hand-rolled Bash scripts](https://github.com/wincent/wincent/tree/master/bin) to replace [virtualenv](https://virtualenv.pypa.io/) and friends ([Python](https://www.python.org/), [pip](https://pypi.org/project/pip/)).
-- [Yarn](https://github.com/yarnpkg/yarn/) ([vendored](https://github.com/wincent/wincent/commit/26adf86d4c742390537be4dc1572f93a97bc3e68)) to install [TypeScript](https://www.typescriptlang.org/).
+- [n](https://github.com/tj/n) ([as a submodule](https://github.com/wincent/wincent/tree/master/vendor)) and some [hand-rolled Bash scripts](https://github.com/wincent/wincent/tree/master/bin) to replace [virtualenv](https://virtualenv.pypa.io/) and friends ([Python](https://www.python.org/), [pip](https://pypi.org/project/pip/)).
+- [Yarn](https://github.com/yarnpkg/yarn/) ([vendored](https://github.com/wincent/wincent/commit/26adf86d4c742390537be4dc1572f93a97bc3e68)) to install [TypeScript](https://www.typescriptlang.org/).
Beyond that, there are no dependencies outside of the [Node.js](https://nodejs.org/en/) standard library. I use [Prettier](https://prettier.io/) to format code, but I invoke it via `npx` which means the [yarn.lock](https://github.com/wincent/wincent/blob/master/yarn.lock) remains basically empty. Ansible itself is replaced by [a set of self-contained TypeScript scripts](https://github.com/wincent/wincent/tree/master/src). Instead of YAML configuration files containing "declarative" configuration peppered with snippets of Python, we just use TypeScript for everything. Instead of [Jinja templates](https://jinja.palletsprojects.com/), we use ERB/JSP-like templates that use embedded JavaScript when necessary.
The repo is written and maintained by Greg Hurrell <[greg@hurrell.net](mailto:greg@hurrell.net)>. Other contributors that have submitted patches include, in alphabetical order:
-- Joe Lencioni
-- Jonathan Wilkins
-- Mark Stenglein
-- Matthew Byrne
-- Stone C. Lasley
-- Victor Igor
-- Zac Collier
+- Joe Lencioni
+- Jonathan Wilkins
+- Mark Stenglein
+- Matthew Byrne
+- Stone C. Lasley
+- Victor Igor
+- Zac Collier
This list produced with:
{
- "description": "Configures launchd"
+ "description": "Configures launchd"
}
import {resource, template, task} from '../../src/Fig';
task('configure (global) LaunchDaemons', async () => {
- const items = [
- {
- path: '/Library/LaunchDaemons/limit.maxfiles.plist',
- variables: {
- arguments: ['limit', 'maxfiles', 65536, 65536],
- label: 'limit.maxfiles',
- },
- },
- {
- path: '/Library/LaunchDaemons/limit.maxproc.plist',
- variables: {
- arguments: ['limit', 'maxproc', 2048, 2048],
- label: 'limit.maxproc',
- },
- },
- ];
+ const items = [
+ {
+ path: '/Library/LaunchDaemons/limit.maxfiles.plist',
+ variables: {
+ arguments: ['limit', 'maxfiles', 65536, 65536],
+ label: 'limit.maxfiles',
+ },
+ },
+ {
+ path: '/Library/LaunchDaemons/limit.maxproc.plist',
+ variables: {
+ arguments: ['limit', 'maxproc', 2048, 2048],
+ label: 'limit.maxproc',
+ },
+ },
+ ];
- for (const {path, variables} of items) {
- await template({
- group: 'wheel',
- mode: '0644',
- owner: 'root',
- path,
- src: resource.template('run.plist.erb'),
- variables,
- });
- }
+ for (const {path, variables} of items) {
+ await template({
+ group: 'wheel',
+ mode: '0644',
+ owner: 'root',
+ path,
+ src: resource.template('run.plist.erb'),
+ variables,
+ });
+ }
});
task('configure (local) LaunchAgents', async () => {
- const items = [
- {
- path: '~/Library/LaunchAgents/setenv.lang.plist',
- variables: {
- arguments: ['setenv', 'LANG', 'en_US.UTF-8'],
- label: 'setenv.lang',
- },
- },
- {
- path: '~/Library/LaunchAgents/setenv.lc_time.plist',
- variables: {
- arguments: ['setenv', 'LC_TIME', 'en_AU.UTF-8'],
- label: 'setenv.lc_time',
- },
- },
- ];
+ const items = [
+ {
+ path: '~/Library/LaunchAgents/setenv.lang.plist',
+ variables: {
+ arguments: ['setenv', 'LANG', 'en_US.UTF-8'],
+ label: 'setenv.lang',
+ },
+ },
+ {
+ path: '~/Library/LaunchAgents/setenv.lc_time.plist',
+ variables: {
+ arguments: ['setenv', 'LC_TIME', 'en_AU.UTF-8'],
+ label: 'setenv.lc_time',
+ },
+ },
+ ];
- for (const {path, variables} of items) {
- await template({
- mode: '0644',
- path,
- src: resource.template('run.plist.erb'),
- variables,
- });
- }
+ for (const {path, variables} of items) {
+ await template({
+ mode: '0644',
+ path,
+ src: resource.template('run.plist.erb'),
+ variables,
+ });
+ }
});
{
- "description": "Sets up terminfo database entries for italics and 256-color support",
- "variables": {
- "terminfo_path": "~/.terminfo"
- }
+ "description": "Sets up terminfo database entries for italics and 256-color support",
+ "variables": {
+ "terminfo_path": "~/.terminfo"
+ }
}
import {command, file, resource, task, variable} from '../../src/Fig';
task('create target directory', async () => {
- await file({
- path: variable.string('terminfo_path'),
- state: 'directory',
- });
+ await file({
+ path: variable.string('terminfo_path'),
+ state: 'directory',
+ });
});
task('update terminfo files', async () => {
- for (const terminfo of resource.files('*.terminfo')) {
- await command('tic', '-o', variable.string('terminfo_path'), terminfo);
- }
+ for (const terminfo of resource.files('*.terminfo')) {
+ await command('tic', '-o', variable.string('terminfo_path'), terminfo);
+ }
});
{
- "name": "wincent",
- "description": "\"dotfiles\" and system configuration",
- "version": "0.0.1",
- "main": "/dev/null",
- "private": true,
- "repository": "https://github.com/wincent/wincent.git",
- "author": "Greg Hurrell <greg@hurrell.net>",
- "license": "Public Domain",
- "scripts": {
- "ci": "yarn format:check",
- "format:check": "npx prettier --check \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool",
- "format": "npx prettier --write \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool"
- },
- "dependencies": {
- "typescript": "3.8.3"
- },
- "devDependencies": {
- "@types/node": "*"
- }
+ "name": "wincent",
+ "description": "\"dotfiles\" and system configuration",
+ "version": "0.0.1",
+ "main": "/dev/null",
+ "private": true,
+ "repository": "https://github.com/wincent/wincent.git",
+ "author": "Greg Hurrell <greg@hurrell.net>",
+ "license": "Public Domain",
+ "scripts": {
+ "ci": "yarn format:check",
+ "format:check": "npx prettier --check \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool",
+ "format": "npx prettier --write \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool"
+ },
+ "dependencies": {
+ "typescript": "3.8.3"
+ },
+ "devDependencies": {
+ "@types/node": "*"
+ }
}
{
- "variables": {
- "corpus_notes": "~/Documents/Corpus",
- "git_cipher_path": "vendor/git-cipher/bin/git-cipher"
- }
+ "variables": {
+ "corpus_notes": "~/Documents/Corpus",
+ "git_cipher_path": "vendor/git-cipher/bin/git-cipher"
+ }
}
{
- "variables": {
- "corpus_notes": "~/Sync/Personal/Corpus"
- }
+ "variables": {
+ "corpus_notes": "~/Sync/Personal/Corpus"
+ }
}
{
- "variables": {}
+ "variables": {}
}
{
- "platforms": {
- "darwin": {
- "aspects": ["launchd", "terminfo"],
- "variables": {}
+ "platforms": {
+ "darwin": {
+ "aspects": ["launchd", "terminfo"],
+ "variables": {}
+ },
+ "linux": {
+ "aspects": ["terminfo"],
+ "variables": {
+ "terminfo_path": "~/share/terminfo"
+ }
+ }
},
- "linux": {
- "aspects": ["terminfo"],
- "variables": {
- "terminfo_path": "~/share/terminfo"
- }
+ "profiles": {
+ "personal": {
+ "pattern": "/\\bretiro\\b/i",
+ "variables": {}
+ },
+ "work": {
+ "pattern": "/\\blfrw044\\b/i",
+ "variables": {}
+ }
}
- },
- "profiles": {
- "personal": {
- "pattern": "/\\bretiro\\b/i",
- "variables": {}
- },
- "work": {
- "pattern": "/\\blfrw044\\b/i",
- "variables": {}
- }
- }
}
// 4. Set all JS files to open via this app.
// 5. Profit.
function run(input, parameters) {
- var iTerm = Application('iTerm2');
- iTerm.activate();
- var windows = iTerm.windows();
- var window;
- var tab;
- if (windows.length) {
- window = iTerm.currentWindow();
- tab = window.createTabWithDefaultProfile();
- } else {
- window = iTerm.createWindowWithDefaultProfile();
- tab = window.currentTab();
- }
- var session = tab.currentSession();
- var files = [];
- for (var i = 0; i < input.length; i++) {
- files.push(quotedForm(input[i]));
- }
- session.write({text: 'vim ' + files.join(' ')});
+ var iTerm = Application('iTerm2');
+ iTerm.activate();
+ var windows = iTerm.windows();
+ var window;
+ var tab;
+ if (windows.length) {
+ window = iTerm.currentWindow();
+ tab = window.createTabWithDefaultProfile();
+ } else {
+ window = iTerm.createWindowWithDefaultProfile();
+ tab = window.currentTab();
+ }
+ var session = tab.currentSession();
+ var files = [];
+ for (var i = 0; i < input.length; i++) {
+ files.push(quotedForm(input[i]));
+ }
+ session.write({text: 'vim ' + files.join(' ')});
}
// Based on: https://ruby-doc.org/stdlib-2.3.0/libdoc/shellwords/rdoc/Shellwords.html#method-c-shellescape
function quotedForm(path) {
- var string = path.toString();
+ var string = path.toString();
- if (string === '' || string === null) {
- return "''";
- }
+ if (string === '' || string === null) {
+ return "''";
+ }
- return string
- .replace(/([^a-z0-9_\-.,:\/@\n])/gi, '\\$1')
- .replace(/\n/g, "'\n'");
+ return string
+ .replace(/([^a-z0-9_\-.,:\/@\n])/gi, '\\$1')
+ .replace(/\n/g, "'\n'");
}
{
- "address": "~/.clipper.sock"
+ "address": "~/.clipper.sock"
}
\ ]
function! wincent#autocmds#apply_overrides(file, type) abort
+ let l:editorconfig=wincent#autocmds#editorconfig(a:file, a:type)
+
+ " TODO: Deal with more kinds of globs
+ " typical examples:
+ " * any file
+ " *.js any js file
+ " lib/**.js any js file under lib (at any level)
+ " *.{js,ts} any js or ts file
+ " meaning of each char:
+ " * match run of any char except /
+ " ** match any run of chars
+ " ? any single char
+ " [abc] any single char of a, b, c
+ " [!abc] any single char not in set a, b, c
+ " {a,b,c} a or b or c (can be nested, gulp)
+ " {-10..10} numbers between -10 and 10
+ " \\ escape
+
+ let l:overrides={}
+ let l:path=fnamemodify(a:file, ':p')
+ for l:config in l:editorconfig
+ " unsilent echomsg string(l:config)
+ let l:glob=l:config.name
+
+ " TODO: don't backslash-escape already-escaped things
+ let l:glob=substitute(l:glob, '\v\.', '\\.', 'g')
+ let l:glob=substitute(l:glob, '\v\*\*', '.+', 'g')
+ let l:glob=substitute(l:glob, '\v\*', '[^/]+', 'g')
+
+ " TODO: replace this hack with real thing:
+ let l:glob=substitute(l:glob, '\v\{', '(', 'g')
+ let l:glob=substitute(l:glob, '\v\}', ')', 'g')
+ let l:glob=substitute(l:glob, '\v,', '|', 'g')
+
+ if match(l:path, '\v' . l:glob . '$') != -1
+ " BUG: won't handle unsaved files; maybe that is ok
+ for l:pair in items(l:config.pairs)
+ let l:key=l:pair[0]
+ let l:value=l:pair[1]
+ if l:key == 'indent_style'
+ if l:value == 'space'
+ setlocal expandtab
+ else
+ setlocal noexpandtab
+ endif
+ elseif l:key == 'indent_size'
+ let &l:shiftwidth=l:value
+ let &l:tabstop=l:value
+ endif
+ endfor
+ endif
+ endfor
+
+ " TODO: rework this so that if when we check for liferay directories, if
+ " found, they return an "editorconfig object...
+
let l:pattern=join(s:wincent_override_filetypes, '\|')
if match(a:type, '\<\(' . l:pattern . '\)\>') != -1
let l:detected=wincent#liferay#detect(a:file)
endif
endfunction
+" For full list of possible keys, see:
+"
+" https://editorconfig-specification.readthedocs.io/en/latest/
+"
+" We only support a restrictive subset for now.
+let s:editorconfig_keys={
+ \ 'indent_style': ['tab', 'space'],
+ \ 'indent_size': ['tab', '1', '2', '3', '4', '5', '6', '7', '8'],
+ \ 'insert_final_newline': ['true', 'false'],
+ \ 'tab_width': ['tab', '1', '2', '3', '4', '5', '6', '7', '8']
+ \ }
+
+" Traverse upwards looking for an .editorconfig.
+"
+" Implements a subset of the functionality described at: https://editorconfig.org/
+"
+" @param {file} path of current file(either absolute or relative to cwd)
+" @param {type} filetype
+"
+" If there is no filename yet, `file` will be the same as `type`.
+function! wincent#autocmds#editorconfig(file, type) abort
+ let l:path=fnamemodify(a:file, ':p')
+ while 1
+ let l:path=fnamemodify(l:path, ':h')
+ let l:candidate=l:path . '/.editorconfig'
+ if filereadable(l:candidate)
+ break
+ endif
+ if l:path == '' || l:path == '/'
+ return []
+ endif
+ endwhile
+
+ let l:config=[]
+ let l:section=v:null
+ let l:lines=readfile(l:candidate)
+ for l:line in l:lines
+ if match(l:line, '\v^\s*$') != -1
+ " Blank line, skip.
+ elseif match(l:line, '\v^\s*[#;]') != -1
+ " Comment, skip.
+ else
+ let l:header=matchlist(l:line, '\v^\s*\[([^\]]+)\]\s*$')
+ if !empty(l:header)
+ " Starting a section.
+ let l:section={'name': l:header[1], 'pairs': {}}
+ call add(l:config, l:section)
+ else
+ let l:pair=matchlist(l:line, '\v^\s*([^=]{-})\s*\=\s*(\S.{-})\s*$')
+ if !empty(l:pair)
+ " Adding key/value pair to current section.
+ let l:key=l:pair[1]
+ let l:value=l:pair[2]
+ if type(l:section) == type(v:null)
+ if l:key == 'root'
+ " 'root' in preamble.
+ "
+ " Possible values: 'true' or 'false'.
+ "
+ " TODO: actually keep walking upward to see if there are
+ " more files to be merged. (Low-pri: I don't expect to see
+ " .editorconfig files at multiple levels in practice very
+ " often; by far the most common pattern is a single file
+ " at the repo root with 'root = true'.)
+ else
+ " Ignore non-'root' key in preamble.
+ endif
+ else
+ if l:key == 'root'
+ " Ignore 'root' outside of preamble.
+ elseif has_key(s:editorconfig_keys, l:key)
+ " Non-'root' outside of preamble.
+ if l:value == 'unset'
+ " Remove the key, if present.
+ if has_key(l:section.pairs, l:key)
+ call remove(l:section.pairs, l:key)
+ endif
+ elseif index(get(s:editorconfig_keys, l:key), l:value) != -1
+ " Legit value for this key.
+ let l:section.pairs[l:key]=l:value
+ else
+ " Invalid/unsupported value.
+ endif
+ else
+ " Unknown key.
+ endif
+ endif
+ else
+ " Unknown format, skip.
+ endif
+ endif
+ endif
+ endfor
+
+ return l:config
+endfunction
+
function! wincent#autocmds#format(motion) abort
if has('ex_extra')
let l:v=operator#user#visual_command_from_wise_name(a:motion)
const {createInterface} = require('readline');
function log(...messages) {
- console.log(...messages);
+ console.log(...messages);
}
function error(...messages) {
- log('error: ', ...messages);
+ log('error: ', ...messages);
}
function die(...messages) {
- if (messages.length) {
- log(...messages);
- }
- process.exit(1);
+ if (messages.length) {
+ log(...messages);
+ }
+ process.exit(1);
}
async function confirm(prompt) {
- const readline = createInterface({
- input: process.stdin,
- output: process.stdout,
- });
-
- return new Promise((resolve) => {
- readline.question(`${prompt} [y/n] `, resolve);
-
- readline.write('y');
- })
- .then((result) => {
- return /^\s*y(es?)?\s*$/i.test(result);
- })
- .finally(() => {
- readline.close();
+ const readline = createInterface({
+ input: process.stdin,
+ output: process.stdout,
});
+
+ return new Promise((resolve) => {
+ readline.question(`${prompt} [y/n] `, resolve);
+
+ readline.write('y');
+ })
+ .then((result) => {
+ return /^\s*y(es?)?\s*$/i.test(result);
+ })
+ .finally(() => {
+ readline.close();
+ });
}
// The lowest signal number (via `man 3 signal`).
const SIGUSR2 = 31;
function run(command, ...args) {
- return new Promise((resolve, reject) => {
- let resolved = false;
- const child = child_process.spawn(command, args, {stdio: 'inherit'});
-
- process.on('SIGINT', () => {
- child.kill('SIGINT');
- });
-
- child.on('error', (err) => {
- if (!resolved) {
- resolved = true;
- reject(err);
- }
- });
-
- child.on('exit', (code) => {
- if (!resolved) {
- resolved = true;
- if (code) {
- let err;
- const description = `\`${[command, ...args].join(' ')}\``;
- if (code >= 128 + SIGHUP && code <= 128 + SIGUSR2) {
- err = new Error(
- `${description} exited due to signal ${code - 128}`
- );
- } else {
- err = new Error(`${description} exited with status ${code}`);
- }
- reject(err);
- } else {
- resolve();
- }
- }
+ return new Promise((resolve, reject) => {
+ let resolved = false;
+ const child = child_process.spawn(command, args, {stdio: 'inherit'});
+
+ process.on('SIGINT', () => {
+ child.kill('SIGINT');
+ });
+
+ child.on('error', (err) => {
+ if (!resolved) {
+ resolved = true;
+ reject(err);
+ }
+ });
+
+ child.on('exit', (code) => {
+ if (!resolved) {
+ resolved = true;
+ if (code) {
+ let err;
+ const description = `\`${[command, ...args].join(' ')}\``;
+ if (code >= 128 + SIGHUP && code <= 128 + SIGUSR2) {
+ err = new Error(
+ `${description} exited due to signal ${code - 128}`
+ );
+ } else {
+ err = new Error(
+ `${description} exited with status ${code}`
+ );
+ }
+ reject(err);
+ } else {
+ resolve();
+ }
+ }
+ });
});
- });
}
async function getHelper(command) {
- if (command.match(/^\w+$/)) {
- const dir = path.join(__dirname, 'portool-helpers');
- const helper = path.join(dir, command);
-
- try {
- fs.accessSync(helper, fs.constants.X_OK);
-
- if (fs.statSync(helper).isFile()) {
- return helper;
- }
- } catch (error) {
- if (error.code === 'ENOENT') {
- // Check for a possible match.
- const candidates = fs
- .readdirSync(dir)
- .filter((entry) => /^[a-z]/.test(entry));
-
- const prefixMatches = candidates.filter((entry) =>
- entry.startsWith(command)
- );
-
- if (prefixMatches.length === 1) {
- // Unambiguous prefix match.
- return getHelper(prefixMatches[0]);
- } else if (prefixMatches.length > 1) {
- // Multiple possible prefix matches.
- log(
- `Ambiguous command ${JSON.stringify(
- command
- )} - did you mean one of?\n` +
- '\n' +
- prefixMatches.map((prefix) => ` ${prefix}`).join('\n') +
- '\n'
- );
- } else {
- // Prompt for confirmation of best fuzzy match guess.
- const fuzzyMatches = candidates
- .map((candidate) => ({
- candidate,
- score: jaroWinkler(candidate, command),
- }))
- .filter(({score}) => score)
- .sort((a, b) => {
- if (a.score < b.score) {
- return -1;
- } else if (a.score > b.score) {
- return 1;
- } else {
- return 0;
- }
- })
- .reverse();
-
- const proposed = fuzzyMatches[0];
-
- if (proposed) {
- log(
- `Command ${JSON.stringify(
- command
- )} invoked, which does not exist.\n`
- );
-
- const proceed = await confirm(
- `Do you want to run ${JSON.stringify(
- proposed.candidate
- )} instead?`
- );
-
- if (proceed) {
- return getHelper(proposed.candidate);
+ if (command.match(/^\w+$/)) {
+ const dir = path.join(__dirname, 'portool-helpers');
+ const helper = path.join(dir, command);
+
+ try {
+ fs.accessSync(helper, fs.constants.X_OK);
+
+ if (fs.statSync(helper).isFile()) {
+ return helper;
+ }
+ } catch (error) {
+ if (error.code === 'ENOENT') {
+ // Check for a possible match.
+ const candidates = fs
+ .readdirSync(dir)
+ .filter((entry) => /^[a-z]/.test(entry));
+
+ const prefixMatches = candidates.filter((entry) =>
+ entry.startsWith(command)
+ );
+
+ if (prefixMatches.length === 1) {
+ // Unambiguous prefix match.
+ return getHelper(prefixMatches[0]);
+ } else if (prefixMatches.length > 1) {
+ // Multiple possible prefix matches.
+ log(
+ `Ambiguous command ${JSON.stringify(
+ command
+ )} - did you mean one of?\n` +
+ '\n' +
+ prefixMatches
+ .map((prefix) => ` ${prefix}`)
+ .join('\n') +
+ '\n'
+ );
+ } else {
+ // Prompt for confirmation of best fuzzy match guess.
+ const fuzzyMatches = candidates
+ .map((candidate) => ({
+ candidate,
+ score: jaroWinkler(candidate, command),
+ }))
+ .filter(({score}) => score)
+ .sort((a, b) => {
+ if (a.score < b.score) {
+ return -1;
+ } else if (a.score > b.score) {
+ return 1;
+ } else {
+ return 0;
+ }
+ })
+ .reverse();
+
+ const proposed = fuzzyMatches[0];
+
+ if (proposed) {
+ log(
+ `Command ${JSON.stringify(
+ command
+ )} invoked, which does not exist.\n`
+ );
+
+ const proceed = await confirm(
+ `Do you want to run ${JSON.stringify(
+ proposed.candidate
+ )} instead?`
+ );
+
+ if (proceed) {
+ return getHelper(proposed.candidate);
+ }
+ }
+ }
}
- }
}
- }
}
- }
}
/**
*
*/
function jaroWinkler(a, b) {
- if (!a.length || !b.length) {
- // Empty strings match nothing, not even each other.
- return 0;
- }
+ if (!a.length || !b.length) {
+ // Empty strings match nothing, not even each other.
+ return 0;
+ }
- const s1 = a.toLowerCase();
- const s2 = b.toLowerCase();
+ const s1 = a.toLowerCase();
+ const s2 = b.toLowerCase();
- if (s1 === s2) {
- // Exact match.
- return 1;
- }
+ if (s1 === s2) {
+ // Exact match.
+ return 1;
+ }
- let m = 0;
+ let m = 0;
- const range = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
- const s1Matches = new Array(s1.length);
- const s2Matches = new Array(s2.length);
+ const range = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
+ const s1Matches = new Array(s1.length);
+ const s2Matches = new Array(s2.length);
- for (let i = 0; i < s1.length; i++) {
- const low = i >= range ? i - range : 0;
- const high = i + range <= s2.length - 1 ? i + range : s2.length - 1;
+ for (let i = 0; i < s1.length; i++) {
+ const low = i >= range ? i - range : 0;
+ const high = i + range <= s2.length - 1 ? i + range : s2.length - 1;
- for (let j = low; j <= high; j++) {
- if (s1Matches[i] !== true && s2Matches[j] !== true && s1[i] === s2[j]) {
- ++m;
+ for (let j = low; j <= high; j++) {
+ if (
+ s1Matches[i] !== true &&
+ s2Matches[j] !== true &&
+ s1[i] === s2[j]
+ ) {
+ ++m;
- s1Matches[i] = s2Matches[j] = true;
+ s1Matches[i] = s2Matches[j] = true;
- break;
- }
+ break;
+ }
+ }
}
- }
- if (m === 0) {
- // No matching letters.
- return 0;
- }
+ if (m === 0) {
+ // No matching letters.
+ return 0;
+ }
- // Count transpositions.
- let k = 0;
- let transpositions = 0;
+ // Count transpositions.
+ let k = 0;
+ let transpositions = 0;
- for (let i = 0; i < s1.length; i++) {
- if (s1Matches[i] === true) {
- let j;
+ for (let i = 0; i < s1.length; i++) {
+ if (s1Matches[i] === true) {
+ let j;
- for (j = k; j < s2.length; j++) {
- if (s2Matches[j] === true) {
- k = j + 1;
- break;
- }
- }
+ for (j = k; j < s2.length; j++) {
+ if (s2Matches[j] === true) {
+ k = j + 1;
+ break;
+ }
+ }
- if (s1[i] !== s2[j]) {
- ++transpositions;
- }
+ if (s1[i] !== s2[j]) {
+ ++transpositions;
+ }
+ }
}
- }
- // "sim" is the Jaro similarity ("sim[j]").
- let sim = (m / s1.length + m / s2.length + (m - transpositions / 2) / m) / 3;
+ // "sim" is the Jaro similarity ("sim[j]").
+ let sim =
+ (m / s1.length + m / s2.length + (m - transpositions / 2) / m) / 3;
- // Apply prefix scaling to obtain Jaro-Winkler similarity ("sim[w]").
+ // Apply prefix scaling to obtain Jaro-Winkler similarity ("sim[w]").
- // Length of common prefix (up to a maximum of 4 characters).
- let l = 0;
+ // Length of common prefix (up to a maximum of 4 characters).
+ let l = 0;
- // Scaling factor. 0.1 is standard, 0.25 is upper limit to prevent similarity
- // from exceeding 1.
- const p = 0.1;
+ // Scaling factor. 0.1 is standard, 0.25 is upper limit to prevent similarity
+ // from exceeding 1.
+ const p = 0.1;
- if (sim > 0.7) {
- while (s1[l] === s2[l] && l < 4) {
- ++l;
- }
+ if (sim > 0.7) {
+ while (s1[l] === s2[l] && l < 4) {
+ ++l;
+ }
- sim = sim + l * p * (1 - sim);
- }
+ sim = sim + l * p * (1 - sim);
+ }
- return sim;
+ return sim;
}
const HELP = 'run `portool help` to see available commands';
async function main(_node, _script, command, ...args) {
- if (!command) {
- die(`must supply command: ${HELP}`);
- }
- const helper = await getHelper(command);
- if (!helper) {
- die(`no such command: ${command} - ${HELP}`);
- }
-
- await run(helper, ...args);
+ if (!command) {
+ die(`must supply command: ${HELP}`);
+ }
+ const helper = await getHelper(command);
+ if (!helper) {
+ die(`no such command: ${command} - ${HELP}`);
+ }
+
+ await run(helper, ...args);
}
main(...process.argv).catch((err) => {
- error(err);
- process.exit(1);
+ error(err);
+ process.exit(1);
});
// vim: ft=javascript
const {bundleIdentifier, deepCopy, isObject, visit} = require('./karabiner');
(function test_bundleIdentifier() {
- (function $() {
- assert(
- bundleIdentifier('com.apple.TextEdit') === '^com\\.apple\\.TextEdit$',
- $
- );
- })();
+ (function $() {
+ assert(
+ bundleIdentifier('com.apple.TextEdit') ===
+ '^com\\.apple\\.TextEdit$',
+ $
+ );
+ })();
})();
(function test_deepCopy() {
- const source = {
- object: {
- isInner: true,
- },
- array: [1, 2, [3, 4]],
- };
- const copy = deepCopy(source);
-
- // Copies look the same.
- (function $() {
- assert(
- JSON.stringify(copy) ===
- '{"object":{"isInner":true},"array":[1,2,[3,4]]}',
- $
- );
- })();
-
- // Objects are cloned.
- (function $() {
- assert(source !== copy, $);
- })();
-
- // Nested objects are cloned.
- (function $() {
- assert(source.object !== copy.object, $);
- })();
-
- // Arrays are cloned.
- (function $() {
- assert(source.array !== copy.array, $);
- })();
-
- // Nested arrays are cloned.
- (function $() {
- assert(source.array[2] !== copy.array[2], $);
- })();
-
- // Primitives are identical.
- (function $() {
- assert(source.array[0] === copy.array[0], $);
- })();
+ const source = {
+ object: {
+ isInner: true,
+ },
+ array: [1, 2, [3, 4]],
+ };
+ const copy = deepCopy(source);
+
+ // Copies look the same.
+ (function $() {
+ assert(
+ JSON.stringify(copy) ===
+ '{"object":{"isInner":true},"array":[1,2,[3,4]]}',
+ $
+ );
+ })();
+
+ // Objects are cloned.
+ (function $() {
+ assert(source !== copy, $);
+ })();
+
+ // Nested objects are cloned.
+ (function $() {
+ assert(source.object !== copy.object, $);
+ })();
+
+ // Arrays are cloned.
+ (function $() {
+ assert(source.array !== copy.array, $);
+ })();
+
+ // Nested arrays are cloned.
+ (function $() {
+ assert(source.array[2] !== copy.array[2], $);
+ })();
+
+ // Primitives are identical.
+ (function $() {
+ assert(source.array[0] === copy.array[0], $);
+ })();
})();
(function test_isObject() {
- // Arrays are not objects.
- (function $() {
- assert(!isObject([1]), $);
- })();
-
- // Booleans are not objects.
- (function $() {
- assert(!isObject(true), $);
- })();
-
- // `null` is not an object.
- (function $() {
- assert(!isObject(null), $);
- })();
-
- // Numbers are not objects.
- (function $() {
- assert(!isObject(1), $);
- })();
-
- // Strings are not objects.
- (function $() {
- assert(!isObject('this'), $);
- })();
-
- // `undefined` is not an object.
- (function $() {
- assert(!isObject(undefined), $);
- })();
-
- // Objects are objects.
- (function $() {
- assert(isObject({}), $);
- })();
+ // Arrays are not objects.
+ (function $() {
+ assert(!isObject([1]), $);
+ })();
+
+ // Booleans are not objects.
+ (function $() {
+ assert(!isObject(true), $);
+ })();
+
+ // `null` is not an object.
+ (function $() {
+ assert(!isObject(null), $);
+ })();
+
+ // Numbers are not objects.
+ (function $() {
+ assert(!isObject(1), $);
+ })();
+
+ // Strings are not objects.
+ (function $() {
+ assert(!isObject('this'), $);
+ })();
+
+ // `undefined` is not an object.
+ (function $() {
+ assert(!isObject(undefined), $);
+ })();
+
+ // Objects are objects.
+ (function $() {
+ assert(isObject({}), $);
+ })();
})();
(function test_visit() {
- const subject = () => ({
- foo: 1,
- bar: [
- {
- a: [{}, {deep: {prop: 3}}],
- },
- {
- a: [],
- b: [{deep: {prop: 10}}],
- },
- ],
- });
-
- // Helpers for readability.
- const string = JSON.stringify;
- const squish = (s) => s.replace(/\s+/g, '');
-
- // Replacing the entire document.
- (function $() {
- const updated = visit(subject(), '$', (root) => 'replacement');
- assert(updated === 'replacement', $);
- })();
-
- // Setting a property on an object.
- (function $() {
- const updated = visit(subject(), '$.foo', (value) => value + 5);
- assert(
- string(updated) ===
- squish(`{
+ const subject = () => ({
+ foo: 1,
+ bar: [
+ {
+ a: [{}, {deep: {prop: 3}}],
+ },
+ {
+ a: [],
+ b: [{deep: {prop: 10}}],
+ },
+ ],
+ });
+
+ // Helpers for readability.
+ const string = JSON.stringify;
+ const squish = (s) => s.replace(/\s+/g, '');
+
+ // Replacing the entire document.
+ (function $() {
+ const updated = visit(subject(), '$', (root) => 'replacement');
+ assert(updated === 'replacement', $);
+ })();
+
+ // Setting a property on an object.
+ (function $() {
+ const updated = visit(subject(), '$.foo', (value) => value + 5);
+ assert(
+ string(updated) ===
+ squish(`{
"foo": 6,
"bar": [
{
}
]
}`),
- $
- );
- })();
-
- // Modifying a list.
- (function $() {
- let counter = 10;
- const updated = visit(subject(), '$.bar[0:]', (_) => counter++);
- assert(
- string(updated) ===
- squish(`{
+ $
+ );
+ })();
+
+ // Modifying a list.
+ (function $() {
+ let counter = 10;
+ const updated = visit(subject(), '$.bar[0:]', (_) => counter++);
+ assert(
+ string(updated) ===
+ squish(`{
"foo": 1,
"bar": [10, 11]
}`),
- $
- );
- })();
-
- // Re-cycling subtrees.
- (function $() {
- const original = subject();
- const updated = visit(original, '$.bar[0:].a[0:].deep', (value) => 'xxx');
-
- assert(
- updated !== original &&
- updated.foo === original.foo &&
- updated.bar !== original.bar &&
- updated.bar[0] !== original.bar[0] &&
- updated.bar[0].a !== original.bar[0].a &&
- updated.bar[0].a[0] !== original.bar[0].a[0] &&
- updated.bar[1] === original.bar[1],
- $
- );
- })();
+ $
+ );
+ })();
+
+ // Re-cycling subtrees.
+ (function $() {
+ const original = subject();
+ const updated = visit(
+ original,
+ '$.bar[0:].a[0:].deep',
+ (value) => 'xxx'
+ );
+
+ assert(
+ updated !== original &&
+ updated.foo === original.foo &&
+ updated.bar !== original.bar &&
+ updated.bar[0] !== original.bar[0] &&
+ updated.bar[0].a !== original.bar[0].a &&
+ updated.bar[0].a[0] !== original.bar[0].a[0] &&
+ updated.bar[1] === original.bar[1],
+ $
+ );
+ })();
})();
#!/usr/bin/env node
function fromTo(from, to) {
- return [
- {
- from: {
- key_code: from,
- },
- to: {
- key_code: to,
- },
- },
- ];
+ return [
+ {
+ from: {
+ key_code: from,
+ },
+ to: {
+ key_code: to,
+ },
+ },
+ ];
}
function bundleIdentifier(identifier) {
- return '^' + identifier.replace(/\./g, '\\.') + '$';
+ return '^' + identifier.replace(/\./g, '\\.') + '$';
}
function spaceFN(from, to) {
- return [
- {
- from: {
- modifiers: {
- optional: ['any'],
- },
- simultaneous: [
- {
- key_code: 'spacebar',
- },
- {
- key_code: from,
- },
- ],
- simultaneous_options: {
- key_down_order: 'strict',
- key_up_order: 'strict_inverse',
- to_after_key_up: [
- {
- set_variable: {
- name: 'SpaceFN',
- value: 0,
- },
- },
- ],
- },
- },
- parameters: {
- 'basic.simultaneous_threshold_milliseconds': 500 /* Default: 1000 */,
- },
- to: [
- {
- set_variable: {
- name: 'SpaceFN',
- value: 1,
- },
- },
- {
- key_code: to,
- },
- ],
- type: 'basic',
- },
- {
- conditions: [
+ return [
{
- name: 'SpaceFN',
- type: 'variable_if',
- value: 1,
- },
- ],
- from: {
- key_code: from,
- modifiers: {
- optional: ['any'],
+ from: {
+ modifiers: {
+ optional: ['any'],
+ },
+ simultaneous: [
+ {
+ key_code: 'spacebar',
+ },
+ {
+ key_code: from,
+ },
+ ],
+ simultaneous_options: {
+ key_down_order: 'strict',
+ key_up_order: 'strict_inverse',
+ to_after_key_up: [
+ {
+ set_variable: {
+ name: 'SpaceFN',
+ value: 0,
+ },
+ },
+ ],
+ },
+ },
+ parameters: {
+ 'basic.simultaneous_threshold_milliseconds': 500 /* Default: 1000 */,
+ },
+ to: [
+ {
+ set_variable: {
+ name: 'SpaceFN',
+ value: 1,
+ },
+ },
+ {
+ key_code: to,
+ },
+ ],
+ type: 'basic',
},
- },
- to: [
{
- key_code: to,
+ conditions: [
+ {
+ name: 'SpaceFN',
+ type: 'variable_if',
+ value: 1,
+ },
+ ],
+ from: {
+ key_code: from,
+ modifiers: {
+ optional: ['any'],
+ },
+ },
+ to: [
+ {
+ key_code: to,
+ },
+ ],
+ type: 'basic',
},
- ],
- type: 'basic',
- },
- ];
+ ];
}
function swap(a, b) {
- return [...fromTo(a, b), ...fromTo(b, a)];
+ return [...fromTo(a, b), ...fromTo(b, a)];
}
const DEVICE_DEFAULTS = {
- disable_built_in_keyboard_if_exists: false,
- fn_function_keys: [],
- ignore: false,
- manipulate_caps_lock_led: true,
- simple_modifications: [],
+ disable_built_in_keyboard_if_exists: false,
+ fn_function_keys: [],
+ ignore: false,
+ manipulate_caps_lock_led: true,
+ simple_modifications: [],
};
const IDENTIFIER_DEFAULTS = {
- is_keyboard: true,
- is_pointing_device: false,
+ is_keyboard: true,
+ is_pointing_device: false,
};
const APPLE_INTERNAL_US = {
- ...DEVICE_DEFAULTS,
- identifiers: {
- ...IDENTIFIER_DEFAULTS,
- product_id: 628,
- vendor_id: 1452,
- },
+ ...DEVICE_DEFAULTS,
+ identifiers: {
+ ...IDENTIFIER_DEFAULTS,
+ product_id: 628,
+ vendor_id: 1452,
+ },
};
const APPLE_INTERNAL_ES = {
- ...DEVICE_DEFAULTS,
- identifiers: {
- ...IDENTIFIER_DEFAULTS,
- product_id: 636,
- vendor_id: 1452,
- },
- simple_modifications: [
- ...fromTo('non_us_backslash', 'grave_accent_and_tilde'),
- ...fromTo('grave_accent_and_tilde', 'left_shift'),
- ...fromTo('backslash', 'return_or_enter'),
- ],
+ ...DEVICE_DEFAULTS,
+ identifiers: {
+ ...IDENTIFIER_DEFAULTS,
+ product_id: 636,
+ vendor_id: 1452,
+ },
+ simple_modifications: [
+ ...fromTo('non_us_backslash', 'grave_accent_and_tilde'),
+ ...fromTo('grave_accent_and_tilde', 'left_shift'),
+ ...fromTo('backslash', 'return_or_enter'),
+ ],
};
const REALFORCE = {
- ...DEVICE_DEFAULTS,
- identifiers: {
- ...IDENTIFIER_DEFAULTS,
- product_id: 273,
- vendor_id: 2131,
- },
- simple_modifications: [
- ...swap('left_command', 'left_option'),
- ...swap('right_command', 'right_option'),
- ...fromTo('application', 'fn'),
- ...fromTo('pause', 'power'),
- ],
+ ...DEVICE_DEFAULTS,
+ identifiers: {
+ ...IDENTIFIER_DEFAULTS,
+ product_id: 273,
+ vendor_id: 2131,
+ },
+ simple_modifications: [
+ ...swap('left_command', 'left_option'),
+ ...swap('right_command', 'right_option'),
+ ...fromTo('application', 'fn'),
+ ...fromTo('pause', 'power'),
+ ],
};
const PARAMETER_DEFAULTS = {
- 'basic.simultaneous_threshold_milliseconds': 50,
- 'basic.to_delayed_action_delay_milliseconds': 500,
- 'basic.to_if_alone_timeout_milliseconds': 1000,
- 'basic.to_if_held_down_threshold_milliseconds': 500,
+ 'basic.simultaneous_threshold_milliseconds': 50,
+ 'basic.to_delayed_action_delay_milliseconds': 500,
+ 'basic.to_if_alone_timeout_milliseconds': 1000,
+ 'basic.to_if_held_down_threshold_milliseconds': 500,
};
const VANILLA_PROFILE = {
- complex_modifications: {
- parameters: PARAMETER_DEFAULTS,
- rules: [],
- },
- devices: [],
- fn_function_keys: [
- ...fromTo('f1', 'display_brightness_decrement'),
- ...fromTo('f2', 'display_brightness_increment'),
- ...fromTo('f3', 'mission_control'),
- ...fromTo('f4', 'launchpad'),
- ...fromTo('f5', 'illumination_decrement'),
- ...fromTo('f6', 'illumination_increment'),
- ...fromTo('f7', 'rewind'),
- ...fromTo('f8', 'play_or_pause'),
- ...fromTo('f9', 'fastforward'),
- ...fromTo('f10', 'mute'),
- ...fromTo('f11', 'volume_decrement'),
- ...fromTo('f12', 'volume_increment'),
- ],
- name: 'Vanilla',
- selected: false,
- simple_modifications: [],
- virtual_hid_keyboard: {
- caps_lock_delay_milliseconds: 0,
- keyboard_type: 'ansi',
- },
+ complex_modifications: {
+ parameters: PARAMETER_DEFAULTS,
+ rules: [],
+ },
+ devices: [],
+ fn_function_keys: [
+ ...fromTo('f1', 'display_brightness_decrement'),
+ ...fromTo('f2', 'display_brightness_increment'),
+ ...fromTo('f3', 'mission_control'),
+ ...fromTo('f4', 'launchpad'),
+ ...fromTo('f5', 'illumination_decrement'),
+ ...fromTo('f6', 'illumination_increment'),
+ ...fromTo('f7', 'rewind'),
+ ...fromTo('f8', 'play_or_pause'),
+ ...fromTo('f9', 'fastforward'),
+ ...fromTo('f10', 'mute'),
+ ...fromTo('f11', 'volume_decrement'),
+ ...fromTo('f12', 'volume_increment'),
+ ],
+ name: 'Vanilla',
+ selected: false,
+ simple_modifications: [],
+ virtual_hid_keyboard: {
+ caps_lock_delay_milliseconds: 0,
+ keyboard_type: 'ansi',
+ },
};
function isObject(item) {
- return (
- item !== null && Object.prototype.toString.call(item) === '[object Object]'
- );
+ return (
+ item !== null &&
+ Object.prototype.toString.call(item) === '[object Object]'
+ );
}
function deepCopy(item) {
- if (Array.isArray(item)) {
- return item.map(deepCopy);
- } else if (isObject(item)) {
- const copy = {};
- Object.entries(item).forEach(([k, v]) => {
- copy[k] = deepCopy(v);
- });
- return copy;
- }
- return item;
+ if (Array.isArray(item)) {
+ return item.map(deepCopy);
+ } else if (isObject(item)) {
+ const copy = {};
+ Object.entries(item).forEach(([k, v]) => {
+ copy[k] = deepCopy(v);
+ });
+ return copy;
+ }
+ return item;
}
/**
* - `[start:end]`: selects an array slice; `end` is optional.
*/
function visit(item, path, updater) {
- const match = path.match(
- /^(?<root>\$)|\.(?<child>\w+)|\[(?<slice>.+?)\]|(?<done>$)/
- );
- const {
- groups: {root, child, slice},
- } = match;
- const subpath = path.slice(match[0].length);
- if (root) {
- return visit(item, subpath, updater);
- } else if (child) {
- const next = visit(item[child], subpath, updater);
- if (next !== undefined) {
- return {
- ...item,
- [child]: next,
- };
- }
- } else if (slice) {
+ const match = path.match(
+ /^(?<root>\$)|\.(?<child>\w+)|\[(?<slice>.+?)\]|(?<done>$)/
+ );
const {
- groups: {start, end},
- } = slice.match(/^(?<start>\d+):(?<end>\d+)?$/);
- let array;
- for (let i = start, max = end == null ? item.length : end; i < max; i++) {
- const next = visit(item[i], subpath, updater);
- if (next !== undefined) {
- if (!array) {
- array = item.slice(0, i);
+ groups: {root, child, slice},
+ } = match;
+ const subpath = path.slice(match[0].length);
+ if (root) {
+ return visit(item, subpath, updater);
+ } else if (child) {
+ const next = visit(item[child], subpath, updater);
+ if (next !== undefined) {
+ return {
+ ...item,
+ [child]: next,
+ };
}
- array[i] = next;
- } else if (array) {
- array[i] = item[i];
- }
+ } else if (slice) {
+ const {
+ groups: {start, end},
+ } = slice.match(/^(?<start>\d+):(?<end>\d+)?$/);
+ let array;
+ for (
+ let i = start, max = end == null ? item.length : end;
+ i < max;
+ i++
+ ) {
+ const next = visit(item[i], subpath, updater);
+ if (next !== undefined) {
+ if (!array) {
+ array = item.slice(0, i);
+ }
+ array[i] = next;
+ } else if (array) {
+ array[i] = item[i];
+ }
+ }
+ return array;
+ } else {
+ const next = updater(item);
+ return next === item ? undefined : next;
}
- return array;
- } else {
- const next = updater(item);
- return next === item ? undefined : next;
- }
}
const EXEMPTIONS = ['com.factorio', 'com.feralinteractive.dirtrally'];
function applyExemptions(profile) {
- const exemptions = {
- type: 'frontmost_application_unless',
- bundle_identifiers: EXEMPTIONS.map(bundleIdentifier),
- };
+ const exemptions = {
+ type: 'frontmost_application_unless',
+ bundle_identifiers: EXEMPTIONS.map(bundleIdentifier),
+ };
- return visit(
- profile,
- '$.complex_modifications.rules[0:].manipulators[0:].conditions',
- (conditions) => {
- if (conditions) {
- if (
- conditions.some(
- (condition) => condition.type === 'frontmost_application_if'
- )
- ) {
- return conditions;
+ return visit(
+ profile,
+ '$.complex_modifications.rules[0:].manipulators[0:].conditions',
+ (conditions) => {
+ if (conditions) {
+ if (
+ conditions.some(
+ (condition) =>
+ condition.type === 'frontmost_application_if'
+ )
+ ) {
+ return conditions;
+ }
+ return [...deepCopy(conditions), exemptions];
+ } else {
+ return [exemptions];
+ }
}
- return [...deepCopy(conditions), exemptions];
- } else {
- return [exemptions];
- }
- }
- );
+ );
}
const DEFAULT_PROFILE = applyExemptions({
- ...VANILLA_PROFILE,
- complex_modifications: {
- parameters: {
- ...PARAMETER_DEFAULTS,
- 'basic.to_if_alone_timeout_milliseconds': 500 /* Default: 1000 */,
- },
- rules: [
- {
- description: 'SpaceFN layer',
- manipulators: [
- ...spaceFN('b', 'spacebar'),
- ...spaceFN('u', 'right_arrow'),
- ...spaceFN('y', 'down_arrow'),
- ...spaceFN('h', 'left_arrow'),
- ...spaceFN('n', 'up_arrow'),
- ...spaceFN('l', 'right_arrow'),
- ...spaceFN('k', 'down_arrow'),
- ...spaceFN('j', 'left_arrow'),
- ...spaceFN('i', 'up_arrow'),
- ],
- },
- {
- description: 'Tab + Return to Backslash',
- manipulators: [
- {
- from: {
- modifiers: {
- optional: ['any'],
- },
- simultaneous: [
- {
- key_code: 'tab',
- },
- {
- key_code: 'return_or_enter',
- },
- ],
- simultaneous_options: {
- key_down_order: 'insensitive',
- key_up_order: 'insensitive',
- },
+ ...VANILLA_PROFILE,
+ complex_modifications: {
+ parameters: {
+ ...PARAMETER_DEFAULTS,
+ 'basic.to_if_alone_timeout_milliseconds': 500 /* Default: 1000 */,
+ },
+ rules: [
+ {
+ description: 'SpaceFN layer',
+ manipulators: [
+ ...spaceFN('b', 'spacebar'),
+ ...spaceFN('u', 'right_arrow'),
+ ...spaceFN('y', 'down_arrow'),
+ ...spaceFN('h', 'left_arrow'),
+ ...spaceFN('n', 'up_arrow'),
+ ...spaceFN('l', 'right_arrow'),
+ ...spaceFN('k', 'down_arrow'),
+ ...spaceFN('j', 'left_arrow'),
+ ...spaceFN('i', 'up_arrow'),
+ ],
},
- to: [
- {
- key_code: 'backslash',
- },
- ],
- conditions: [
- {
- type: 'device_if',
- identifiers: [APPLE_INTERNAL_ES.identifiers],
- },
- ],
- type: 'basic',
- },
- ],
- },
- {
- description:
- 'Disable Karabiner-Elements with Fn+Control+Option+Command+Z',
- manipulators: [
- {
- type: 'basic',
- from: {
- key_code: 'z',
- modifiers: {
- mandatory: [
- 'fn',
- 'left_control',
- 'left_command',
- 'left_option',
+ {
+ description: 'Tab + Return to Backslash',
+ manipulators: [
+ {
+ from: {
+ modifiers: {
+ optional: ['any'],
+ },
+ simultaneous: [
+ {
+ key_code: 'tab',
+ },
+ {
+ key_code: 'return_or_enter',
+ },
+ ],
+ simultaneous_options: {
+ key_down_order: 'insensitive',
+ key_up_order: 'insensitive',
+ },
+ },
+ to: [
+ {
+ key_code: 'backslash',
+ },
+ ],
+ conditions: [
+ {
+ type: 'device_if',
+ identifiers: [APPLE_INTERNAL_ES.identifiers],
+ },
+ ],
+ type: 'basic',
+ },
],
- },
},
- to: [
- {
- shell_command: 'osascript ~/.zsh/bin/karabiner-kill.scpt',
- },
- ],
- },
- ],
- },
- {
- description:
- 'Change Caps Lock to Control when used as modifier, Backspace when used alone',
- manipulators: [
- {
- from: {
- key_code: 'caps_lock',
- modifiers: {
- optional: ['any'],
- },
+ {
+ description:
+ 'Disable Karabiner-Elements with Fn+Control+Option+Command+Z',
+ manipulators: [
+ {
+ type: 'basic',
+ from: {
+ key_code: 'z',
+ modifiers: {
+ mandatory: [
+ 'fn',
+ 'left_control',
+ 'left_command',
+ 'left_option',
+ ],
+ },
+ },
+ to: [
+ {
+ shell_command:
+ 'osascript ~/.zsh/bin/karabiner-kill.scpt',
+ },
+ ],
+ },
+ ],
},
- to: [
- {
- key_code: 'left_control',
- lazy: true,
- },
- ],
- to_if_alone: [
- {
- key_code: 'delete_or_backspace',
- },
- ],
- to_if_held_down: [
- {
- key_code: 'delete_or_backspace',
- },
- ],
- type: 'basic',
- },
- ],
- },
- {
- description:
- 'Change Return to Control when used as modifier, Return when used alone',
- manipulators: [
- {
- from: {
- key_code: 'return_or_enter',
- modifiers: {
- optional: ['any'],
- },
+ {
+ description:
+ 'Change Caps Lock to Control when used as modifier, Backspace when used alone',
+ manipulators: [
+ {
+ from: {
+ key_code: 'caps_lock',
+ modifiers: {
+ optional: ['any'],
+ },
+ },
+ to: [
+ {
+ key_code: 'left_control',
+ lazy: true,
+ },
+ ],
+ to_if_alone: [
+ {
+ key_code: 'delete_or_backspace',
+ },
+ ],
+ to_if_held_down: [
+ {
+ key_code: 'delete_or_backspace',
+ },
+ ],
+ type: 'basic',
+ },
+ ],
},
- to: [
- {
- key_code: 'right_control',
- lazy: true,
- },
- ],
- to_if_alone: [
- {
- key_code: 'return_or_enter',
- },
- ],
- to_if_held_down: [
- {
- key_code: 'return_or_enter',
- },
- ],
- type: 'basic',
- },
- ],
- },
- {
- description: 'Change Control+I to F6 in Vim',
- manipulators: [
- {
- conditions: [
- {
- bundle_identifiers: [
- bundleIdentifier('com.apple.Terminal'),
- bundleIdentifier('com.googlecode.iterm2'),
- bundleIdentifier('org.vim.MacVim.plist'),
+ {
+ description:
+ 'Change Return to Control when used as modifier, Return when used alone',
+ manipulators: [
+ {
+ from: {
+ key_code: 'return_or_enter',
+ modifiers: {
+ optional: ['any'],
+ },
+ },
+ to: [
+ {
+ key_code: 'right_control',
+ lazy: true,
+ },
+ ],
+ to_if_alone: [
+ {
+ key_code: 'return_or_enter',
+ },
+ ],
+ to_if_held_down: [
+ {
+ key_code: 'return_or_enter',
+ },
+ ],
+ type: 'basic',
+ },
],
- type: 'frontmost_application_if',
- },
- ],
- from: {
- key_code: 'l',
- modifiers: {
- mandatory: ['control'],
- optional: ['any'],
- },
},
- to: [
- {
- key_code: 'f6',
- modifiers: ['fn'],
- },
- ],
- type: 'basic',
- },
- ],
- },
- {
- description: 'Left and Right Shift together toggle Caps Lock',
- manipulators: [
- {
- from: {
- modifiers: {
- optional: ['any'],
- },
- simultaneous: [
- {
- key_code: 'left_shift',
- },
- {
- key_code: 'right_shift',
- },
- ],
- simultaneous_options: {
- key_down_order: 'insensitive',
- key_up_order: 'insensitive',
- },
+ {
+ description: 'Change Control+I to F6 in Vim',
+ manipulators: [
+ {
+ conditions: [
+ {
+ bundle_identifiers: [
+ bundleIdentifier('com.apple.Terminal'),
+ bundleIdentifier('com.googlecode.iterm2'),
+ bundleIdentifier('org.vim.MacVim.plist'),
+ ],
+ type: 'frontmost_application_if',
+ },
+ ],
+ from: {
+ key_code: 'l',
+ modifiers: {
+ mandatory: ['control'],
+ optional: ['any'],
+ },
+ },
+ to: [
+ {
+ key_code: 'f6',
+ modifiers: ['fn'],
+ },
+ ],
+ type: 'basic',
+ },
+ ],
+ },
+ {
+ description: 'Left and Right Shift together toggle Caps Lock',
+ manipulators: [
+ {
+ from: {
+ modifiers: {
+ optional: ['any'],
+ },
+ simultaneous: [
+ {
+ key_code: 'left_shift',
+ },
+ {
+ key_code: 'right_shift',
+ },
+ ],
+ simultaneous_options: {
+ key_down_order: 'insensitive',
+ key_up_order: 'insensitive',
+ },
+ },
+ to: [
+ {
+ key_code: 'caps_lock',
+ },
+ ],
+ type: 'basic',
+ },
+ ],
},
- to: [
- {
- key_code: 'caps_lock',
- },
- ],
- type: 'basic',
- },
],
- },
- ],
- },
- devices: [REALFORCE, APPLE_INTERNAL_US, APPLE_INTERNAL_ES],
- name: 'Default',
- selected: true,
+ },
+ devices: [REALFORCE, APPLE_INTERNAL_US, APPLE_INTERNAL_ES],
+ name: 'Default',
+ selected: true,
});
const CONFIG = {
- global: {
- check_for_updates_on_startup: true,
- show_in_menu_bar: true,
- show_profile_name_in_menu_bar: false,
- },
- profiles: [DEFAULT_PROFILE, VANILLA_PROFILE],
+ global: {
+ check_for_updates_on_startup: true,
+ show_in_menu_bar: true,
+ show_profile_name_in_menu_bar: false,
+ },
+ profiles: [DEFAULT_PROFILE, VANILLA_PROFILE],
};
if (require.main === module) {
- // Script is being executed directly.
- process.stdout.write(JSON.stringify(CONFIG, null, 2) + '\n');
+ // Script is being executed directly.
+ process.stdout.write(JSON.stringify(CONFIG, null, 2) + '\n');
} else {
- // File is being `require`-ed as a module.
- module.exports = {
- bundleIdentifier,
- deepCopy,
- isObject,
- visit,
- };
+ // File is being `require`-ed as a module.
+ module.exports = {
+ bundleIdentifier,
+ deepCopy,
+ isObject,
+ visit,
+ };
}
{
- "Profiles": [
- {
- "Name": "Base",
- "Guid": "D70AD504-EFA6-41E2-AF8C-7EC47B16C6B8",
- "Custom Directory": "No",
- "Set Local Environment Vars": true,
- "Working Directory": "/Users/glh",
- "Prompt Before Closing 2": 0,
- "Rows": 25,
- "Use Italic Font": true,
- "Use Custom Window Title": false,
- "Right Option Key Sends": 0,
- "Character Encoding": 4,
- "Triggers": [],
- "Blend": 0.300000011920929,
- "Mouse Reporting": true,
- "Cursor Boost": 0,
- "Non-ASCII Anti Aliased": true,
- "Sync Title": false,
- "Disable Window Resizing": true,
- "Close Sessions On End": true,
- "Jobs to Ignore": ["rlogin", "ssh", "slogin", "telnet"],
- "Scrollback With Status Bar": false,
- "Scrollback Lines": 0,
- "Scrollback in Alternate Screen": false,
- "Hide After Opening": false,
- "Flashing Bell": false,
- "BM Growl": false,
- "AWDS Window Directory": "",
- "Use Non-ASCII Font": false,
- "Shortcut": "",
- "Background Image Location": "",
- "Unlimited Scrollback": false,
- "Custom Command": "No",
- "AWDS Tab Option": "No",
- "Smart Selection Rules": [
+ "Profiles": [
{
- "notes": "Word bounded by whitespace",
- "regex": "\\S+",
- "precision": "low"
- },
- {
- "notes": "C++ namespace::identifier",
- "regex": "([a-zA-Z0-9_]+::)+[a-zA-Z0-9_]+",
- "precision": "normal"
- },
- {
- "notes": "Paths",
- "regex": "\\~?/?([[:letter:][:number:]._-]+/+)+[[:letter:][:number:]._-]+/?",
- "precision": "normal"
- },
- {
- "notes": "Quoted string",
- "regex": "@?\"(?:[^\"\\\\]|\\\\.)*\"",
- "precision": "normal"
- },
- {
- "notes": "Java/Python include paths",
- "regex": "([[:letter:][:number:]._]+\\.)+[[:letter:][:number:]._]+",
- "precision": "normal"
- },
- {
- "notes": "mailto URL",
- "regex": "\\bmailto:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
- "precision": "normal"
- },
- {
- "notes": "Obj-C selector",
- "regex": "@selector\\([^)]+\\)",
- "precision": "high"
- },
- {
- "notes": "email address",
- "regex": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b",
- "precision": "high"
- },
- {
- "notes": "HTTP URL",
- "regex": "https?://([a-z0-9A-Z]+(:[a-zA-Z0-9]+)?@)?[a-z0-9A-Z]+(\\.[a-z0-9A-Z]+)*((:[0-9]+)?)(/[a-zA-Z0-9;/\\.\\-_+%~?&@=#\\(\\)]*)?",
- "precision": "very_high"
- },
- {
- "notes": "SSH URL",
- "regex": "\\bssh:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
- "precision": "very_high"
- },
- {
- "notes": "Telnet URL",
- "regex": "\\btelnet:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
- "precision": "very_high"
- }
- ],
- "Keyboard Map": {
- "0x9-0x40000": {
- "Text": "9",
- "Action": 11
- },
- "0xf700-0x260000": {
- "Text": "[1;6A",
- "Action": 10
- },
- "0x37-0x40000": {
- "Text": "0x1f",
- "Action": 11
- },
- "0x32-0x40000": {
- "Text": "0x00",
- "Action": 11
- },
- "0xf709-0x20000": {
- "Text": "[17;2~",
- "Action": 10
- },
- "0xf70c-0x20000": {
- "Text": "[20;2~",
- "Action": 10
- },
- "0xf729-0x20000": {
- "Text": "[1;2H",
- "Action": 10
- },
- "0xf72b-0x40000": {
- "Text": "[1;5F",
- "Action": 10
- },
- "0xf705-0x20000": {
- "Text": "[1;2Q",
- "Action": 10
- },
- "0xf703-0x260000": {
- "Text": "[1;6C",
- "Action": 10
- },
- "0xf700-0x220000": {
- "Text": "[1;2A",
- "Action": 10
- },
- "0xf701-0x280000": {
- "Text": "0x1b 0x1b 0x5b 0x42",
- "Action": 11
- },
- "0x38-0x40000": {
- "Text": "0x7f",
- "Action": 11
- },
- "0x33-0x40000": {
- "Text": "0x1b",
- "Action": 11
- },
- "0xf703-0x220000": {
- "Text": "[1;2C",
- "Action": 10
- },
- "0xf701-0x240000": {
- "Text": "[1;5B",
- "Action": 10
- },
- "0xf70d-0x20000": {
- "Text": "[21;2~",
- "Action": 10
- },
- "0xf702-0x260000": {
- "Text": "[1;6D",
- "Action": 10
- },
- "0xf729-0x40000": {
- "Text": "[1;5H",
- "Action": 10
- },
- "0xf706-0x20000": {
- "Text": "[1;2R",
- "Action": 10
- },
- "0x34-0x40000": {
- "Text": "0x1c",
- "Action": 11
- },
- "0xf700-0x280000": {
- "Text": "0x1b 0x1b 0x5b 0x41",
- "Action": 11
- },
- "0x2d-0x40000": {
- "Text": "0x1f",
- "Action": 11
- },
- "0xf70e-0x20000": {
- "Text": "[23;2~",
- "Action": 10
- },
- "0xf702-0x220000": {
- "Text": "[1;2D",
- "Action": 10
- },
- "0xf703-0x280000": {
- "Text": "f",
- "Action": 10
- },
- "0xf700-0x240000": {
- "Text": "[1;5A",
- "Action": 10
- },
- "0xf707-0x20000": {
- "Text": "[1;2S",
- "Action": 10
- },
- "0xf70a-0x20000": {
- "Text": "[18;2~",
- "Action": 10
- },
- "0x35-0x40000": {
- "Text": "0x1d",
- "Action": 11
- },
- "0xf70f-0x20000": {
- "Text": "[24;2~",
- "Action": 10
- },
- "0xf703-0x240000": {
- "Text": "[1;5C",
- "Action": 10
- },
- "0xf701-0x260000": {
- "Text": "[1;6B",
- "Action": 10
- },
- "0xf702-0x280000": {
- "Text": "b",
- "Action": 10
- },
- "0xf72b-0x20000": {
- "Text": "[1;2F",
- "Action": 10
- },
- "0x36-0x40000": {
- "Text": "0x1e",
- "Action": 11
- },
- "0xf708-0x20000": {
- "Text": "[15;2~",
- "Action": 10
- },
- "0xf701-0x220000": {
- "Text": "[1;2B",
- "Action": 10
- },
- "0xf70b-0x20000": {
- "Text": "[19;2~",
- "Action": 10
- },
- "0xf702-0x240000": {
- "Text": "[1;5D",
- "Action": 10
- },
- "0xf704-0x20000": {
- "Text": "[1;2P",
- "Action": 10
+ "Name": "Base",
+ "Guid": "D70AD504-EFA6-41E2-AF8C-7EC47B16C6B8",
+ "Custom Directory": "No",
+ "Set Local Environment Vars": true,
+ "Working Directory": "/Users/glh",
+ "Prompt Before Closing 2": 0,
+ "Rows": 25,
+ "Use Italic Font": true,
+ "Use Custom Window Title": false,
+ "Right Option Key Sends": 0,
+ "Character Encoding": 4,
+ "Triggers": [],
+ "Blend": 0.300000011920929,
+ "Mouse Reporting": true,
+ "Cursor Boost": 0,
+ "Non-ASCII Anti Aliased": true,
+ "Sync Title": false,
+ "Disable Window Resizing": true,
+ "Close Sessions On End": true,
+ "Jobs to Ignore": ["rlogin", "ssh", "slogin", "telnet"],
+ "Scrollback With Status Bar": false,
+ "Scrollback Lines": 0,
+ "Scrollback in Alternate Screen": false,
+ "Hide After Opening": false,
+ "Flashing Bell": false,
+ "BM Growl": false,
+ "AWDS Window Directory": "",
+ "Use Non-ASCII Font": false,
+ "Shortcut": "",
+ "Background Image Location": "",
+ "Unlimited Scrollback": false,
+ "Custom Command": "No",
+ "AWDS Tab Option": "No",
+ "Smart Selection Rules": [
+ {
+ "notes": "Word bounded by whitespace",
+ "regex": "\\S+",
+ "precision": "low"
+ },
+ {
+ "notes": "C++ namespace::identifier",
+ "regex": "([a-zA-Z0-9_]+::)+[a-zA-Z0-9_]+",
+ "precision": "normal"
+ },
+ {
+ "notes": "Paths",
+ "regex": "\\~?/?([[:letter:][:number:]._-]+/+)+[[:letter:][:number:]._-]+/?",
+ "precision": "normal"
+ },
+ {
+ "notes": "Quoted string",
+ "regex": "@?\"(?:[^\"\\\\]|\\\\.)*\"",
+ "precision": "normal"
+ },
+ {
+ "notes": "Java/Python include paths",
+ "regex": "([[:letter:][:number:]._]+\\.)+[[:letter:][:number:]._]+",
+ "precision": "normal"
+ },
+ {
+ "notes": "mailto URL",
+ "regex": "\\bmailto:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+ "precision": "normal"
+ },
+ {
+ "notes": "Obj-C selector",
+ "regex": "@selector\\([^)]+\\)",
+ "precision": "high"
+ },
+ {
+ "notes": "email address",
+ "regex": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b",
+ "precision": "high"
+ },
+ {
+ "notes": "HTTP URL",
+ "regex": "https?://([a-z0-9A-Z]+(:[a-zA-Z0-9]+)?@)?[a-z0-9A-Z]+(\\.[a-z0-9A-Z]+)*((:[0-9]+)?)(/[a-zA-Z0-9;/\\.\\-_+%~?&@=#\\(\\)]*)?",
+ "precision": "very_high"
+ },
+ {
+ "notes": "SSH URL",
+ "regex": "\\bssh:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+ "precision": "very_high"
+ },
+ {
+ "notes": "Telnet URL",
+ "regex": "\\btelnet:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+ "precision": "very_high"
+ }
+ ],
+ "Keyboard Map": {
+ "0x9-0x40000": {
+ "Text": "9",
+ "Action": 11
+ },
+ "0xf700-0x260000": {
+ "Text": "[1;6A",
+ "Action": 10
+ },
+ "0x37-0x40000": {
+ "Text": "0x1f",
+ "Action": 11
+ },
+ "0x32-0x40000": {
+ "Text": "0x00",
+ "Action": 11
+ },
+ "0xf709-0x20000": {
+ "Text": "[17;2~",
+ "Action": 10
+ },
+ "0xf70c-0x20000": {
+ "Text": "[20;2~",
+ "Action": 10
+ },
+ "0xf729-0x20000": {
+ "Text": "[1;2H",
+ "Action": 10
+ },
+ "0xf72b-0x40000": {
+ "Text": "[1;5F",
+ "Action": 10
+ },
+ "0xf705-0x20000": {
+ "Text": "[1;2Q",
+ "Action": 10
+ },
+ "0xf703-0x260000": {
+ "Text": "[1;6C",
+ "Action": 10
+ },
+ "0xf700-0x220000": {
+ "Text": "[1;2A",
+ "Action": 10
+ },
+ "0xf701-0x280000": {
+ "Text": "0x1b 0x1b 0x5b 0x42",
+ "Action": 11
+ },
+ "0x38-0x40000": {
+ "Text": "0x7f",
+ "Action": 11
+ },
+ "0x33-0x40000": {
+ "Text": "0x1b",
+ "Action": 11
+ },
+ "0xf703-0x220000": {
+ "Text": "[1;2C",
+ "Action": 10
+ },
+ "0xf701-0x240000": {
+ "Text": "[1;5B",
+ "Action": 10
+ },
+ "0xf70d-0x20000": {
+ "Text": "[21;2~",
+ "Action": 10
+ },
+ "0xf702-0x260000": {
+ "Text": "[1;6D",
+ "Action": 10
+ },
+ "0xf729-0x40000": {
+ "Text": "[1;5H",
+ "Action": 10
+ },
+ "0xf706-0x20000": {
+ "Text": "[1;2R",
+ "Action": 10
+ },
+ "0x34-0x40000": {
+ "Text": "0x1c",
+ "Action": 11
+ },
+ "0xf700-0x280000": {
+ "Text": "0x1b 0x1b 0x5b 0x41",
+ "Action": 11
+ },
+ "0x2d-0x40000": {
+ "Text": "0x1f",
+ "Action": 11
+ },
+ "0xf70e-0x20000": {
+ "Text": "[23;2~",
+ "Action": 10
+ },
+ "0xf702-0x220000": {
+ "Text": "[1;2D",
+ "Action": 10
+ },
+ "0xf703-0x280000": {
+ "Text": "f",
+ "Action": 10
+ },
+ "0xf700-0x240000": {
+ "Text": "[1;5A",
+ "Action": 10
+ },
+ "0xf707-0x20000": {
+ "Text": "[1;2S",
+ "Action": 10
+ },
+ "0xf70a-0x20000": {
+ "Text": "[18;2~",
+ "Action": 10
+ },
+ "0x35-0x40000": {
+ "Text": "0x1d",
+ "Action": 11
+ },
+ "0xf70f-0x20000": {
+ "Text": "[24;2~",
+ "Action": 10
+ },
+ "0xf703-0x240000": {
+ "Text": "[1;5C",
+ "Action": 10
+ },
+ "0xf701-0x260000": {
+ "Text": "[1;6B",
+ "Action": 10
+ },
+ "0xf702-0x280000": {
+ "Text": "b",
+ "Action": 10
+ },
+ "0xf72b-0x20000": {
+ "Text": "[1;2F",
+ "Action": 10
+ },
+ "0x36-0x40000": {
+ "Text": "0x1e",
+ "Action": 11
+ },
+ "0xf708-0x20000": {
+ "Text": "[15;2~",
+ "Action": 10
+ },
+ "0xf701-0x220000": {
+ "Text": "[1;2B",
+ "Action": 10
+ },
+ "0xf70b-0x20000": {
+ "Text": "[19;2~",
+ "Action": 10
+ },
+ "0xf702-0x240000": {
+ "Text": "[1;5D",
+ "Action": 10
+ },
+ "0xf704-0x20000": {
+ "Text": "[1;2P",
+ "Action": 10
+ }
+ },
+ "Log Directory": "",
+ "Use Canonical Parser": false,
+ "Background Image Is Tiled": false,
+ "Send Code When Idle": false,
+ "ASCII Anti Aliased": true,
+ "Tags": [],
+ "Use Bold Font": true,
+ "Silence Bell": true,
+ "Window Type": 12,
+ "Allow Title Reporting": false,
+ "Use Bright Bold": true,
+ "Default Bookmark": "No",
+ "Disable Smcup Rmcup": false,
+ "Blinking Cursor": false,
+ "Idle Code": 0,
+ "Automatically Log": false,
+ "Smart Cursor Color": true,
+ "Semantic History": {
+ "text": "vim \\1",
+ "action": "best editor",
+ "editor": "com.sublimetext.3"
+ },
+ "Ambiguous Double Width": false,
+ "Blur Radius": 15.08386418269231,
+ "Cursor Type": 2,
+ "AWDS Pane Directory": "",
+ "Blur": false,
+ "Normal Font": "SourceCodePro-Light 13",
+ "Vertical Spacing": 1,
+ "Disable Printing": false,
+ "AWDS Tab Directory": "",
+ "AWDS Pane Option": "No",
+ "Command": "",
+ "Terminal Type": "xterm-256color",
+ "Horizontal Spacing": 1,
+ "Option Key Sends": 0,
+ "Only The Default BG Color Uses Transparency": false,
+ "Blink Allowed": false,
+ "Minimum Contrast": 0.0,
+ "Transparency": 0,
+ "Initial Text": "",
+ "Screen": -1,
+ "AWDS Window Option": "No",
+ "Non Ascii Font": "Consolas 13",
+ "Columns": 80,
+ "Visual Bell": true,
+ "ASCII Ligatures": false,
+ "Thin Strokes": 1
}
- },
- "Log Directory": "",
- "Use Canonical Parser": false,
- "Background Image Is Tiled": false,
- "Send Code When Idle": false,
- "ASCII Anti Aliased": true,
- "Tags": [],
- "Use Bold Font": true,
- "Silence Bell": true,
- "Window Type": 12,
- "Allow Title Reporting": false,
- "Use Bright Bold": true,
- "Default Bookmark": "No",
- "Disable Smcup Rmcup": false,
- "Blinking Cursor": false,
- "Idle Code": 0,
- "Automatically Log": false,
- "Smart Cursor Color": true,
- "Semantic History": {
- "text": "vim \\1",
- "action": "best editor",
- "editor": "com.sublimetext.3"
- },
- "Ambiguous Double Width": false,
- "Blur Radius": 15.08386418269231,
- "Cursor Type": 2,
- "AWDS Pane Directory": "",
- "Blur": false,
- "Normal Font": "SourceCodePro-Light 13",
- "Vertical Spacing": 1,
- "Disable Printing": false,
- "AWDS Tab Directory": "",
- "AWDS Pane Option": "No",
- "Command": "",
- "Terminal Type": "xterm-256color",
- "Horizontal Spacing": 1,
- "Option Key Sends": 0,
- "Only The Default BG Color Uses Transparency": false,
- "Blink Allowed": false,
- "Minimum Contrast": 0.0,
- "Transparency": 0,
- "Initial Text": "",
- "Screen": -1,
- "AWDS Window Option": "No",
- "Non Ascii Font": "Consolas 13",
- "Columns": 80,
- "Visual Bell": true,
- "ASCII Ligatures": false,
- "Thin Strokes": 1
- }
- ]
+ ]
}
{
- "Profiles": [
- {
- "Name": "Mutt (Base)",
- "Guid": "2AAB5F82-9007-41DA-B772-5E776FD41383",
- "Dynamic Profile Parent Name": "Base",
- "Custom Command": "Yes",
- "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/mutt -- $$URL$$\""
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Mutt (Base)",
+ "Guid": "2AAB5F82-9007-41DA-B772-5E776FD41383",
+ "Dynamic Profile Parent Name": "Base",
+ "Custom Command": "Yes",
+ "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/mutt -- $$URL$$\""
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Vim (Base)",
- "Guid": "8C012B27-65E8-470D-83FA-B5485479863D",
- "Dynamic Profile Parent Name": "Base",
- "Custom Command": "Yes",
- "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/nvim $$RES$$\""
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Vim (Base)",
+ "Guid": "8C012B27-65E8-470D-83FA-B5485479863D",
+ "Dynamic Profile Parent Name": "Base",
+ "Custom Command": "Yes",
+ "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/nvim $$RES$$\""
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Wincent",
- "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
- "Dynamic Profile Parent Name": "Base",
- "Normal Font": "SourceCodePro-Light 16"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Wincent",
+ "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
+ "Dynamic Profile Parent Name": "Base",
+ "Normal Font": "SourceCodePro-Light 16"
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Wincent",
- "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
- "Dynamic Profile Parent Name": "Base",
- "Normal Font": "SourceCodePro-Light 13"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Wincent",
+ "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
+ "Dynamic Profile Parent Name": "Base",
+ "Normal Font": "SourceCodePro-Light 13"
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Mutt",
- "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
- "Dynamic Profile Parent Name": "Mutt (Base)",
- "Normal Font": "SourceCodePro-Light 16"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Mutt",
+ "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
+ "Dynamic Profile Parent Name": "Mutt (Base)",
+ "Normal Font": "SourceCodePro-Light 16"
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Mutt",
- "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
- "Dynamic Profile Parent Name": "Mutt (Base)",
- "Normal Font": "SourceCodePro-Light 13"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Mutt",
+ "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
+ "Dynamic Profile Parent Name": "Mutt (Base)",
+ "Normal Font": "SourceCodePro-Light 13"
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Vim",
- "Guid": "1770FCDD-BCA2-403B-BABF-A6B2D994D725",
- "Dynamic Profile Parent Name": "Vim (Base)",
- "Normal Font": "SourceCodePro-Light 16"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Vim",
+ "Guid": "1770FCDD-BCA2-403B-BABF-A6B2D994D725",
+ "Dynamic Profile Parent Name": "Vim (Base)",
+ "Normal Font": "SourceCodePro-Light 16"
+ }
+ ]
}
{
- "Profiles": [
- {
- "Name": "Vim",
- "Guid": "18A475CE-8744-4C20-AFAE-AAEBF4F20F40",
- "Dynamic Profile Parent Name": "Vim (Base)",
- "Normal Font": "SourceCodePro-Light 13"
- }
- ]
+ "Profiles": [
+ {
+ "Name": "Vim",
+ "Guid": "18A475CE-8744-4C20-AFAE-AAEBF4F20F40",
+ "Dynamic Profile Parent Name": "Vim (Base)",
+ "Normal Font": "SourceCodePro-Light 13"
+ }
+ ]
}
* Immutable system attributes (read-only).
*/
export default class Attributes {
- #homedir?: string;
- #platform?: 'darwin' | 'linux';
- #uid?: number;
- #username?: string;
-
- get homedir(): string {
- if (!this.#homedir) {
- this.#homedir = os.homedir();
- }
+ #homedir?: string;
+ #platform?: 'darwin' | 'linux';
+ #uid?: number;
+ #username?: string;
- return this.#homedir;
- }
+ get homedir(): string {
+ if (!this.#homedir) {
+ this.#homedir = os.homedir();
+ }
- get platform(): 'darwin' | 'linux' {
- if (!this.#platform) {
- const uname = os.type();
+ return this.#homedir;
+ }
- if (uname === 'Darwin') {
- this.#platform = 'darwin';
- } else if (uname === 'Linux') {
- this.#platform = 'linux';
- } else {
- throw new Error(`Unsupported platform ${JSON.stringify(uname)}`);
- }
+ get platform(): 'darwin' | 'linux' {
+ if (!this.#platform) {
+ const uname = os.type();
+
+ if (uname === 'Darwin') {
+ this.#platform = 'darwin';
+ } else if (uname === 'Linux') {
+ this.#platform = 'linux';
+ } else {
+ throw new Error(
+ `Unsupported platform ${JSON.stringify(uname)}`
+ );
+ }
+ }
+
+ return this.#platform;
}
- return this.#platform;
- }
+ get uid(): number {
+ if (typeof this.#uid !== 'number') {
+ this.#uid =
+ typeof process.getuid === 'function' ? process.getuid() : -1;
+ }
- get uid(): number {
- if (typeof this.#uid !== 'number') {
- this.#uid = typeof process.getuid === 'function' ? process.getuid() : -1;
+ return this.#uid;
}
- return this.#uid;
- }
+ get username(): string {
+ if (!this.#username) {
+ this.#username = os.userInfo().username;
+ }
- get username(): string {
- if (!this.#username) {
- this.#username = os.userInfo().username;
+ return this.#username;
}
-
- return this.#username;
- }
}
* Template compiler that manages a cache of compiled templates.
*/
export default class Compiler {
- #compiled: Map<string, {fill: (scope: Scope) => string}>;
+ #compiled: Map<string, {fill: (scope: Scope) => string}>;
- constructor() {
- this.#compiled = new Map();
- }
+ constructor() {
+ this.#compiled = new Map();
+ }
- async compile(path: string): Promise<{fill: (scope: Scope) => string}> {
- const map = this.#compiled;
+ async compile(path: string): Promise<{fill: (scope: Scope) => string}> {
+ const map = this.#compiled;
- if (!map.has(path)) {
- const source = await readFile(path, 'utf8');
+ if (!map.has(path)) {
+ const source = await readFile(path, 'utf8');
- const compiled = compile(source);
+ const compiled = compile(source);
- map.set(path, {
- fill(scope) {
- return fill(compiled, scope);
- },
- });
- }
+ map.set(path, {
+ fill(scope) {
+ return fill(compiled, scope);
+ },
+ });
+ }
- return map.get(path)!;
- }
+ return map.get(path)!;
+ }
}
export type Metadata = {[key: string]: JSONValue};
export default class ErrorWithMetadata extends Error {
- metadata?: Metadata;
+ metadata?: Metadata;
- constructor(message: string, metadata?: Metadata) {
- super(message);
+ constructor(message: string, metadata?: Metadata) {
+ super(message);
- this.metadata = metadata;
- }
+ this.metadata = metadata;
+ }
}
import type {Aspect} from '../types/Project';
type Counts = {
- changed: number;
- failed: number;
- ok: number;
- skipped: number;
+ changed: number;
+ failed: number;
+ ok: number;
+ skipped: number;
};
/**
* possible.
*/
class Context {
- #attributes: Attributes;
- #compiler: Compiler;
- #counts: Counts;
- #currentAspect?: Aspect;
- #currentVariables?: Variables;
- #sudoPassphrase?: Promise<string>;
- #tasks: TaskRegistry;
-
- constructor() {
- this.#attributes = new Attributes();
- this.#compiler = new Compiler();
-
- this.#counts = {
- changed: 0,
- failed: 0,
- ok: 0,
- skipped: 0,
- };
-
- this.#tasks = new TaskRegistry();
- }
-
- compile(path: string) {
- return this.#compiler.compile(path);
- }
-
- informChanged(message: string) {
- this.#counts.changed++;
-
- status.changed(message);
- }
-
- /**
- * @overload
- */
- informFailed(error: ErrorWithMetadata): never;
-
- /**
- * @overload
- */
- informFailed(message: string, metadata?: Metadata): never;
-
- informFailed(...args: Array<any>): never {
- let error: ErrorWithMetadata;
-
- if (typeof args[0] === 'string') {
- error = new ErrorWithMetadata(args[0], args[1]);
- } else {
- error = args[0];
+ #attributes: Attributes;
+ #compiler: Compiler;
+ #counts: Counts;
+ #currentAspect?: Aspect;
+ #currentVariables?: Variables;
+ #sudoPassphrase?: Promise<string>;
+ #tasks: TaskRegistry;
+
+ constructor() {
+ this.#attributes = new Attributes();
+ this.#compiler = new Compiler();
+
+ this.#counts = {
+ changed: 0,
+ failed: 0,
+ ok: 0,
+ skipped: 0,
+ };
+
+ this.#tasks = new TaskRegistry();
}
- this.#counts.failed++;
+ compile(path: string) {
+ return this.#compiler.compile(path);
+ }
- status.failed(error.message);
+ informChanged(message: string) {
+ this.#counts.changed++;
- throw error;
- }
+ status.changed(message);
+ }
- informOk(message: string) {
- this.#counts.ok++;
+ /**
+ * @overload
+ */
+ informFailed(error: ErrorWithMetadata): never;
- status.ok(message);
- }
+ /**
+ * @overload
+ */
+ informFailed(message: string, metadata?: Metadata): never;
- informSkipped(message: string) {
- this.#counts.skipped++;
+ informFailed(...args: Array<any>): never {
+ let error: ErrorWithMetadata;
- status.skipped(message);
- }
+ if (typeof args[0] === 'string') {
+ error = new ErrorWithMetadata(args[0], args[1]);
+ } else {
+ error = args[0];
+ }
- async withContext(
- {aspect, variables}: {aspect: Aspect; variables: Variables},
- callback: () => Promise<void>
- ) {
- let previousAspect = this.#currentAspect;
- let previousVariables = this.#currentVariables;
+ this.#counts.failed++;
- try {
- this.#currentAspect = aspect;
- this.#currentVariables = variables;
+ status.failed(error.message);
- await callback();
- } finally {
- this.#currentAspect = previousAspect;
- this.#currentVariables = previousVariables;
+ throw error;
}
- }
- get attributes(): Attributes {
- return this.#attributes;
- }
+ informOk(message: string) {
+ this.#counts.ok++;
- get counts() {
- return this.#counts;
- }
+ status.ok(message);
+ }
- get currentAspect(): Aspect {
- assert(this.#currentAspect);
+ informSkipped(message: string) {
+ this.#counts.skipped++;
- return this.#currentAspect!;
- }
+ status.skipped(message);
+ }
- set currentAspect(aspect: Aspect) {
- this.#currentAspect = aspect;
- }
+ async withContext(
+ {aspect, variables}: {aspect: Aspect; variables: Variables},
+ callback: () => Promise<void>
+ ) {
+ let previousAspect = this.#currentAspect;
+ let previousVariables = this.#currentVariables;
+
+ try {
+ this.#currentAspect = aspect;
+ this.#currentVariables = variables;
+
+ await callback();
+ } finally {
+ this.#currentAspect = previousAspect;
+ this.#currentVariables = previousVariables;
+ }
+ }
- get currentVariables(): Variables {
- assert(this.#currentVariables);
+ get attributes(): Attributes {
+ return this.#attributes;
+ }
+
+ get counts() {
+ return this.#counts;
+ }
- return this.#currentVariables!;
- }
+ get currentAspect(): Aspect {
+ assert(this.#currentAspect);
- set currentVariables(variables: Variables) {
- this.#currentVariables = variables;
- }
+ return this.#currentAspect!;
+ }
- get sudoPassphrase(): Promise<string> {
- if (!this.#sudoPassphrase) {
- this.#sudoPassphrase = prompt(`Password [will not be echoed]: `, {
- private: true,
- });
+ set currentAspect(aspect: Aspect) {
+ this.#currentAspect = aspect;
}
- return this.#sudoPassphrase;
- }
+ get currentVariables(): Variables {
+ assert(this.#currentVariables);
- // TODO: note that I might be going overboard here with private
- // variables for stuff that I really don't have to worry about getting
- // meddled with
- get tasks(): TaskRegistry {
- return this.#tasks;
- }
+ return this.#currentVariables!;
+ }
+
+ set currentVariables(variables: Variables) {
+ this.#currentVariables = variables;
+ }
+
+ get sudoPassphrase(): Promise<string> {
+ if (!this.#sudoPassphrase) {
+ this.#sudoPassphrase = prompt(`Password [will not be echoed]: `, {
+ private: true,
+ });
+ }
+
+ return this.#sudoPassphrase;
+ }
+
+ // TODO: note that I might be going overboard here with private
+ // variables for stuff that I really don't have to worry about getting
+ // meddled with
+ get tasks(): TaskRegistry {
+ return this.#tasks;
+ }
}
export default new Context();
type Callback = () => Promise<void>;
export default class TaskRegistry {
- #callbacks: Map<Aspect, Array<[Callback, string]>>;
+ #callbacks: Map<Aspect, Array<[Callback, string]>>;
- constructor() {
- this.#callbacks = new Map();
- }
-
- register(aspect: Aspect, callback: Callback, name: string) {
- if (!this.#callbacks.has(aspect)) {
- this.#callbacks.set(aspect, []);
+ constructor() {
+ this.#callbacks = new Map();
}
- this.#callbacks.get(aspect)!.push([callback, name]);
- }
+ register(aspect: Aspect, callback: Callback, name: string) {
+ if (!this.#callbacks.has(aspect)) {
+ this.#callbacks.set(aspect, []);
+ }
+
+ this.#callbacks.get(aspect)!.push([callback, name]);
+ }
- get(aspect: Aspect): Array<[Callback, string]> {
- return this.#callbacks.get(aspect) || [];
- }
+ get(aspect: Aspect): Array<[Callback, string]> {
+ return this.#callbacks.get(aspect) || [];
+ }
}
* Helper to get fixtures (in "src/") irrespective of where we run from.
*/
function fixture(...components: Array<string>): string {
- return join(root, 'src', 'Fig', '__tests__', '__fixtures__', ...components);
+ return join(root, 'src', 'Fig', '__tests__', '__fixtures__', ...components);
}
describe('compare()', () => {
- describe('with {state: file} (implied)', () => {
- test('indicates when the file exists', async () => {
- const path = fixture('sample');
+ describe('with {state: file} (implied)', () => {
+ test('indicates when the file exists', async () => {
+ const path = fixture('sample');
- const diff = await compare({path});
+ const diff = await compare({path});
- expect(diff).toEqual({path});
- });
+ expect(diff).toEqual({path});
+ });
- test('indicates when contents match', async () => {
- const path = fixture('sample');
+ test('indicates when contents match', async () => {
+ const path = fixture('sample');
- const diff = await compare({path, contents: 'sample contents\n'});
+ const diff = await compare({path, contents: 'sample contents\n'});
- expect(diff).toEqual({
- path,
- });
- });
+ expect(diff).toEqual({
+ path,
+ });
+ });
- test('indicates when contents do not match', async () => {
- const path = fixture('sample');
+ test('indicates when contents do not match', async () => {
+ const path = fixture('sample');
- const diff = await compare({path, contents: 'something'});
+ const diff = await compare({path, contents: 'something'});
- expect(diff).toEqual({
- contents: 'something',
- path,
- });
+ expect(diff).toEqual({
+ contents: 'something',
+ path,
+ });
+ });
});
- });
- describe('with {state: file} (explicit)', () => {
- test('returns an "empty" object for files that match', async () => {
- const path = fixture('sample');
+ describe('with {state: file} (explicit)', () => {
+ test('returns an "empty" object for files that match', async () => {
+ const path = fixture('sample');
- const diff = await compare({path, state: 'file'});
+ const diff = await compare({path, state: 'file'});
- expect(diff).toEqual({path});
- });
+ expect(diff).toEqual({path});
+ });
- test('returns {state: "file"} for non-existent files', async () => {
- const path = fixture('non-existent');
+ test('returns {state: "file"} for non-existent files', async () => {
+ const path = fixture('non-existent');
- const diff = await compare({path, state: 'file'});
+ const diff = await compare({path, state: 'file'});
- expect(diff).toEqual({
- path,
- state: 'file',
- });
- });
+ expect(diff).toEqual({
+ path,
+ state: 'file',
+ });
+ });
- test('complains if parent directory does not exist', async () => {
- const path = fixture('does', 'not', 'exist');
+ test('complains if parent directory does not exist', async () => {
+ const path = fixture('does', 'not', 'exist');
- const diff = await compare({path, state: 'file'});
+ const diff = await compare({path, state: 'file'});
- expect(diff.path).toEqual(path);
- expect(diff.error!.message).toMatch(
- /Cannot stat ".+" because parent ".+" does not exist/
- );
+ expect(diff.path).toEqual(path);
+ expect(diff.error!.message).toMatch(
+ /Cannot stat ".+" because parent ".+" does not exist/
+ );
+ });
});
- });
- describe('with {state: "absent"}', () => {
- test('returns an "empty" object for missing files', async () => {
- const path = fixture('non-existent');
+ describe('with {state: "absent"}', () => {
+ test('returns an "empty" object for missing files', async () => {
+ const path = fixture('non-existent');
- const diff = await compare({path, state: 'absent'});
+ const diff = await compare({path, state: 'absent'});
- expect(diff).toEqual({
- path,
- });
+ expect(diff).toEqual({
+ path,
+ });
+ });
});
- });
});
* required with respect to that property.
*/
type Diff = {
- contents?: string;
- error?: Error;
- force?: boolean;
- group?: string;
- mode?: Mode;
- owner?: string;
- readonly path: string;
- state?: 'absent' | 'directory' | 'file' | 'link' | 'touch';
+ contents?: string;
+ error?: Error;
+ force?: boolean;
+ group?: string;
+ mode?: Mode;
+ owner?: string;
+ readonly path: string;
+ state?: 'absent' | 'directory' | 'file' | 'link' | 'touch';
};
type Compare = Omit<Diff, 'error'>;
* made to the current file system to produce that desired end-state.
*/
export default async function compare({
- contents,
- force,
- group,
- mode,
- owner,
- path,
- state = 'file',
+ contents,
+ force,
+ group,
+ mode,
+ owner,
+ path,
+ state = 'file',
}: Compare) {
- const diff: Diff = {path};
+ const diff: Diff = {path};
- const stats = await stat(path);
+ const stats = await stat(path);
- // BUG: if you specify "owner": "root", we should be able to manage files that
- // only root can stat, but this code stats as an unprivileged user
- // (to fix this, port to use stat.ts instead
- if (stats instanceof Error) {
- // Can't stat; bail.
- diff.error = stats;
- return diff;
- } else if (stats === null) {
- // Object does not exist.
- if (state === 'absent') {
- // Want "absent", have "absent": no state change required.
- } else {
- // Distinguish between `path` itself not existing (in which case
- // it can be created), and one of its parents not existing (in which case
- // we have to bail).
- const parent = dirname(path);
- const stats = await stat(parent);
- if (stats instanceof Error) {
- // Unlikely (we were able to stat object but not its parent).
+ // BUG: if you specify "owner": "root", we should be able to manage files that
+ // only root can stat, but this code stats as an unprivileged user
+ // (to fix this, port to use stat.ts instead
+ if (stats instanceof Error) {
+ // Can't stat; bail.
diff.error = stats;
return diff;
- } else if (stats === null) {
- diff.error = new ErrorWithMetadata(
- `Cannot stat ${stringify(path)} because parent ${stringify(
- parent
- )} does not exist`
- );
- } else {
- // Parent exists.
- diff.state = state;
- }
+ } else if (stats === null) {
+ // Object does not exist.
+ if (state === 'absent') {
+ // Want "absent", have "absent": no state change required.
+ } else {
+ // Distinguish between `path` itself not existing (in which case
+ // it can be created), and one of its parents not existing (in which case
+ // we have to bail).
+ const parent = dirname(path);
+ const stats = await stat(parent);
+ if (stats instanceof Error) {
+ // Unlikely (we were able to stat object but not its parent).
+ diff.error = stats;
+ return diff;
+ } else if (stats === null) {
+ diff.error = new ErrorWithMetadata(
+ `Cannot stat ${stringify(path)} because parent ${stringify(
+ parent
+ )} does not exist`
+ );
+ } else {
+ // Parent exists.
+ diff.state = state;
+ }
+ }
+ // Nothing else we can check without the object existing.
+ return diff;
}
- // Nothing else we can check without the object existing.
- return diff;
- }
- // Object exists.
- if (state === 'file') {
- if (stats.type === 'file') {
- // Want "file", have "file": no state change required.
- } else if (stats.type === 'link') {
- // Going to have to overwrite symlink.
- diff.force = true;
- diff.state = 'file';
- } else if (stats.type === 'directory') {
- diff.error = new ErrorWithMetadata(
- `Cannot replace directory ${stringify(path)} with file`
- );
- } else {
- // We're not going to bother with "exotic" types such as sockets etc.
- diff.error = new ErrorWithMetadata(
- `Cannot replace object ${stringify(path)} of unknown type with file`
- );
- }
+ // Object exists.
+ if (state === 'file') {
+ if (stats.type === 'file') {
+ // Want "file", have "file": no state change required.
+ } else if (stats.type === 'link') {
+ // Going to have to overwrite symlink.
+ diff.force = true;
+ diff.state = 'file';
+ } else if (stats.type === 'directory') {
+ diff.error = new ErrorWithMetadata(
+ `Cannot replace directory ${stringify(path)} with file`
+ );
+ } else {
+ // We're not going to bother with "exotic" types such as sockets etc.
+ diff.error = new ErrorWithMetadata(
+ `Cannot replace object ${stringify(
+ path
+ )} of unknown type with file`
+ );
+ }
- if (typeof contents === 'string') {
- try {
- const actual = await fs.readFile(path, 'utf8');
- if (actual !== contents) {
- diff.contents = contents;
+ if (typeof contents === 'string') {
+ try {
+ const actual = await fs.readFile(path, 'utf8');
+ if (actual !== contents) {
+ diff.contents = contents;
+ }
+ } catch (error) {
+ // TODO: if this is a perms issue, that might be ok as long as user has
+ // specified "user"
+ }
}
- } catch (error) {
- // TODO: if this is a perms issue, that might be ok as long as user has
- // specified "user"
- }
- }
- } else if (state === 'directory') {
- if (stats.type === 'directory') {
- // Want "directory", have "directory": no state change required.
- } else if (stats.type === 'file' || stats.type === 'link') {
- if (force) {
- // Will have to remove file/link before creating directory.
- diff.force = true;
- diff.state = state;
- } else {
- const entity = stats.type === 'file' ? 'file' : 'symbolic link';
+ } else if (state === 'directory') {
+ if (stats.type === 'directory') {
+ // Want "directory", have "directory": no state change required.
+ } else if (stats.type === 'file' || stats.type === 'link') {
+ if (force) {
+ // Will have to remove file/link before creating directory.
+ diff.force = true;
+ diff.state = state;
+ } else {
+ const entity = stats.type === 'file' ? 'file' : 'symbolic link';
- diff.error = new ErrorWithMetadata(
- `Cannot replace ${entity} ${stringify(
- path
- )} with directory without 'force'`
- );
- }
- } else {
- // We're not going to bother with "exotic" types such as sockets etc.
- diff.error = new ErrorWithMetadata(
- `Cannot replace object ${stringify(
- path
- )} of unknown type with directory`
- );
+ diff.error = new ErrorWithMetadata(
+ `Cannot replace ${entity} ${stringify(
+ path
+ )} with directory without 'force'`
+ );
+ }
+ } else {
+ // We're not going to bother with "exotic" types such as sockets etc.
+ diff.error = new ErrorWithMetadata(
+ `Cannot replace object ${stringify(
+ path
+ )} of unknown type with directory`
+ );
+ }
+ } else if (state === 'link') {
+ // TODO
+ } else if (state === 'absent') {
+ // TODO
+ } else if (state === 'touch') {
+ // TODO
}
- } else if (state === 'link') {
- // TODO
- } else if (state === 'absent') {
- // TODO
- } else if (state === 'touch') {
- // TODO
- }
- return diff;
+ return diff;
}
* Just supports simple globs ("*") for now.
*/
export default function globToRegExp(glob: string): RegExp {
- const pattern = escapeRegExpPattern(glob);
+ const pattern = escapeRegExpPattern(glob);
- return new RegExp(pattern.replace(/\\\*/g, '[^/]+'));
+ return new RegExp(pattern.replace(/\\\*/g, '[^/]+'));
}
export {variable};
export interface Fig {
- command: typeof command;
- file: typeof file;
- resource: typeof resource;
- root: typeof root;
- task: typeof task;
- template: typeof template;
- variable: typeof variable;
+ command: typeof command;
+ file: typeof file;
+ resource: typeof resource;
+ root: typeof root;
+ task: typeof task;
+ template: typeof template;
+ variable: typeof variable;
}
* Implements basic shell expansion (of ~).
*/
export default async function command(
- executable: string,
- ...args: Array<string>
+ executable: string,
+ ...args: Array<string>
): Promise<void> {
- const description = [executable, ...args].join(' ');
+ const description = [executable, ...args].join(' ');
- try {
- await spawn(expand(executable), ...args.map(expand));
- // TODO: decide whether to log full command here
- Context.informChanged(`command \`${description}\``);
- } catch (error) {
- if (error instanceof ErrorWithMetadata) {
- Context.informFailed(error);
- } else {
- Context.informFailed(`command \`${description}\` failed`);
+ try {
+ await spawn(expand(executable), ...args.map(expand));
+ // TODO: decide whether to log full command here
+ Context.informChanged(`command \`${description}\``);
+ } catch (error) {
+ if (error instanceof ErrorWithMetadata) {
+ Context.informFailed(error);
+ } else {
+ Context.informFailed(`command \`${description}\` failed`);
+ }
}
- }
}
// TODO decide whether we want a separate "directory" operation
// TODO: implement auto-expand of ~
export default async function file({
- force,
- mode,
- path,
- src,
- state,
+ force,
+ mode,
+ path,
+ src,
+ state,
}: {
- path: string;
- mode?: Mode;
- src?: string;
- state: 'directory' | 'file' | 'link' | 'touch';
- force?: boolean;
+ path: string;
+ mode?: Mode;
+ src?: string;
+ state: 'directory' | 'file' | 'link' | 'touch';
+ force?: boolean;
}): Promise<void> {
- if (state === 'directory') {
- directory(path);
- }
+ if (state === 'directory') {
+ directory(path);
+ }
}
function directory(path: string) {
- const target = expand(path);
+ const target = expand(path);
- // TODO: find out if ansible replaces regular file with dir or just errors?
- // TODO: actually throw for errors
- if (fs.existsSync(target)) {
- try {
- const stat = fs.statSync(target);
+ // TODO: find out if ansible replaces regular file with dir or just errors?
+ // TODO: actually throw for errors
+ if (fs.existsSync(target)) {
+ try {
+ const stat = fs.statSync(target);
- if (stat.isDirectory()) {
- Context.informOk(`directory ${path}`);
- } else {
- log.error(`${path} already exists but is not a directory`);
- }
- } catch (error) {
- log.error(`Failed to stat: ${path}`);
+ if (stat.isDirectory()) {
+ Context.informOk(`directory ${path}`);
+ } else {
+ log.error(`${path} already exists but is not a directory`);
+ }
+ } catch (error) {
+ log.error(`Failed to stat: ${path}`);
+ }
+ } else {
+ Context.informChanged(`directory ${path}`);
+ fs.mkdirSync(target, {recursive: true});
}
- } else {
- Context.informChanged(`directory ${path}`);
- fs.mkdirSync(target, {recursive: true});
- }
}
import compare from '../compare';
export default async function template({
- force,
- group,
- mode,
- owner,
- path,
- src,
- variables = {},
-}: {
- force?: boolean;
- group?: string;
- path: string;
- mode?: Mode;
- owner?: string;
- src: string;
- variables: Variables;
-}): Promise<void> {
- const target = expand(path);
-
- const contents = (await Context.compile(src)).fill({variables});
-
- const diff = await compare({
- contents,
force,
group,
mode,
owner,
- path: target,
- state: 'file',
- });
+ path,
+ src,
+ variables = {},
+}: {
+ force?: boolean;
+ group?: string;
+ path: string;
+ mode?: Mode;
+ owner?: string;
+ src: string;
+ variables: Variables;
+}): Promise<void> {
+ const target = expand(path);
- if (owner && owner !== Context.attributes.username) {
- log.debug(`Needs sudo: ${Context.attributes.username} -> ${owner}`);
- const passphrase = await Context.sudoPassphrase;
- const result = await run('ls', ['-l', '/var/audit'], {passphrase});
+ const contents = (await Context.compile(src)).fill({variables});
- if (result.status !== 0) {
- throw new ErrorWithMetadata(`Failed command`, {
- ...result,
- error: result.error?.toString() ?? null,
- });
- }
- } else {
- if (diff.contents) {
- // log.info('change!');
- const temp = await tempfile(contents);
+ const diff = await compare({
+ contents,
+ force,
+ group,
+ mode,
+ owner,
+ path: target,
+ state: 'file',
+ });
- // TODO: cp from temp to target
- // TODO: deal with group/owner/mode etc
+ if (owner && owner !== Context.attributes.username) {
+ log.debug(`Needs sudo: ${Context.attributes.username} -> ${owner}`);
+ const passphrase = await Context.sudoPassphrase;
+ const result = await run('ls', ['-l', '/var/audit'], {passphrase});
+
+ if (result.status !== 0) {
+ throw new ErrorWithMetadata(`Failed command`, {
+ ...result,
+ error: result.error?.toString() ?? null,
+ });
+ }
} else {
- Context.informOk(`template ${path}`);
+ if (diff.contents) {
+ // log.info('change!');
+ const temp = await tempfile(contents);
+
+ // TODO: cp from temp to target
+ // TODO: deal with group/owner/mode etc
+ } else {
+ Context.informOk(`template ${path}`);
+ }
}
- }
}
* active aspect (eg. `aspects/${aspect}/files/${name}`).
*/
export function file(...path: Array<string>): string {
- return join('aspects', Context.currentAspect, 'files', ...path);
+ return join('aspects', Context.currentAspect, 'files', ...path);
}
/**
* Very simple glob-based file search (doesn't supported nested directories).
*/
export function files(glob: string): Array<string> {
- const aspect = Context.currentAspect;
+ const aspect = Context.currentAspect;
- const regExp = globToRegExp(glob);
+ const regExp = globToRegExp(glob);
- return fs
- .readdirSync(join('aspects', aspect, 'files'), {withFileTypes: true})
- .filter((entry) => entry.isFile())
- .map(({name}) => name)
- .filter((name) => regExp.test(name))
- .map((name) => join('aspects', aspect, 'files', name));
+ return fs
+ .readdirSync(join('aspects', aspect, 'files'), {withFileTypes: true})
+ .filter((entry) => entry.isFile())
+ .map(({name}) => name)
+ .filter((name) => regExp.test(name))
+ .map((name) => join('aspects', aspect, 'files', name));
}
export function template(...path: Array<string>): string {
- return join('aspects', Context.currentAspect, 'templates', ...path);
+ return join('aspects', Context.currentAspect, 'templates', ...path);
}
* (compiled).
*/
const root = (function find(path): string {
- const target = 'yarn.lock';
+ const target = 'yarn.lock';
- if (existsSync(join(path, target))) {
- return path;
- } else {
- const next = dirname(path);
+ if (existsSync(join(path, target))) {
+ return path;
+ } else {
+ const next = dirname(path);
- if (next === path) {
- throw new Error(`Searched up to ${path} without finding ${target}`);
- }
+ if (next === path) {
+ throw new Error(`Searched up to ${path} without finding ${target}`);
+ }
- return find(dirname(path));
- }
+ return find(dirname(path));
+ }
})(__dirname);
export default root;
// decide whether we need any of those.
export function changed(message: string) {
- log.notice(`Changed: ${message}`);
+ log.notice(`Changed: ${message}`);
}
export function failed(message: string) {
- log.error(`Failed: ${message}`);
+ log.error(`Failed: ${message}`);
}
export function ok(message: string) {
- log.info(`Ok: ${message}`);
+ log.info(`Ok: ${message}`);
}
export function skipped(message: string) {
- log.info(`Skipped: ${message}`);
+ log.info(`Skipped: ${message}`);
}
import {default as root} from './root';
export default function task(name: string, callback: () => Promise<void>) {
- const caller = getCaller();
+ const caller = getCaller();
- const ancestors = relative(root, caller).split(sep);
+ const ancestors = relative(root, caller).split(sep);
- const aspect =
- ancestors[0] === 'lib' && ancestors[1] === 'aspects' && ancestors[2];
+ const aspect =
+ ancestors[0] === 'lib' && ancestors[1] === 'aspects' && ancestors[2];
- if (!aspect) {
- throw new Error(`Unable to determine aspect for ${caller}`);
- }
+ if (!aspect) {
+ throw new Error(`Unable to determine aspect for ${caller}`);
+ }
- assertAspect(aspect);
+ assertAspect(aspect);
- // TODO: we use `caller` to make namespaced task name.
- // (will be useful for --start-at)
- // also, we can make an interactive mode that lets us choose where to start
- Context.tasks.register(aspect, callback, `${aspect} | ${name}`);
+ // TODO: we use `caller` to make namespaced task name.
+ // (will be useful for --start-at)
+ // also, we can make an interactive mode that lets us choose where to start
+ Context.tasks.register(aspect, callback, `${aspect} | ${name}`);
}
import Context from './Context';
export default function variable(
- name: string,
- fallback?: JSONValue
+ name: string,
+ fallback?: JSONValue
): JSONValue {
- const variables = Context.currentVariables;
+ const variables = Context.currentVariables;
- return variables.hasOwnProperty(name) ? variables[name] : fallback || null;
+ return variables.hasOwnProperty(name) ? variables[name] : fallback || null;
}
variable.string = (name: string, fallback?: JSONValue): string => {
- const value = variable(name, fallback);
+ const value = variable(name, fallback);
- assert(
- typeof value === 'string',
- `Expected variable ${name} to have type string but it was ${typeof value}`
- );
+ assert(
+ typeof value === 'string',
+ `Expected variable ${name} to have type string but it was ${typeof value}`
+ );
- return value;
+ return value;
};
import merge from '../merge';
test('merge() returns an single object', () => {
- expect(merge({example: 'obj'})).toEqual({example: 'obj'});
+ expect(merge({example: 'obj'})).toEqual({example: 'obj'});
});
test('merge() merges two objects with non-overlapping keys', () => {
- expect(merge({example: 'obj'}, {more: 'stuff'})).toEqual({
- example: 'obj',
- more: 'stuff',
- });
+ expect(merge({example: 'obj'}, {more: 'stuff'})).toEqual({
+ example: 'obj',
+ more: 'stuff',
+ });
});
test('merge() merges two objects with overlapping keys', () => {
- expect(
- merge({example: 'obj', more: 'things'}, {more: 'stuff', and: true})
- ).toEqual({example: 'obj', more: 'stuff', and: true});
+ expect(
+ merge({example: 'obj', more: 'things'}, {more: 'stuff', and: true})
+ ).toEqual({example: 'obj', more: 'stuff', and: true});
});
test('merge() overwrites arrays', () => {
- expect(merge({list: [1, 2, 3]}, {list: ['a', 'b', 'c', 'd']})).toEqual({
- list: ['a', 'b', 'c', 'd'],
- });
+ expect(merge({list: [1, 2, 3]}, {list: ['a', 'b', 'c', 'd']})).toEqual({
+ list: ['a', 'b', 'c', 'd'],
+ });
});
test('merge() deep-merges objects', () => {
- expect(
- merge(
- {thing: true, nested: {prop: 'value'}},
- {thing: true, nested: {other: false}}
- )
- ).toEqual({
- thing: true,
- nested: {
- prop: 'value',
- other: false,
- },
- });
+ expect(
+ merge(
+ {thing: true, nested: {prop: 'value'}},
+ {thing: true, nested: {other: false}}
+ )
+ ).toEqual({
+ thing: true,
+ nested: {
+ prop: 'value',
+ other: false,
+ },
+ });
});
import regExpFromString from '../regExpFromString';
test('regExpFromString() returns a RegExp', () => {
- const regExp = regExpFromString('/\\bword\\b/');
+ const regExp = regExpFromString('/\\bword\\b/');
- expect(regExp instanceof RegExp).toBe(true);
- expect(regExp.source).toBe('\\bword\\b');
- expect(regExp.flags).toBe('');
+ expect(regExp instanceof RegExp).toBe(true);
+ expect(regExp.source).toBe('\\bword\\b');
+ expect(regExp.flags).toBe('');
});
test('regExpFromString() preserves flags', () => {
- const regExp = regExpFromString('/^foo/mig');
+ const regExp = regExpFromString('/^foo/mig');
- expect(regExp instanceof RegExp).toBe(true);
- expect(regExp.source).toBe('^foo');
- expect(regExp.flags).toBe('gim');
+ expect(regExp instanceof RegExp).toBe(true);
+ expect(regExp.source).toBe('^foo');
+ expect(regExp.flags).toBe('gim');
});
test('regExpFromString() rejects an invalid pattern', () => {
- expect(() => regExpFromString('thing')).toThrow(
- 'Invalid pattern "thing" does not match /^\\/(.+)\\/([gimsuy]*)$/'
- );
+ expect(() => regExpFromString('thing')).toThrow(
+ 'Invalid pattern "thing" does not match /^\\/(.+)\\/([gimsuy]*)$/'
+ );
});
import stringify from '../stringify';
test('stringify() null', () => {
- expect(stringify(null)).toBe('null');
+ expect(stringify(null)).toBe('null');
});
test('stringify() undefined', () => {
- expect(stringify(undefined)).toBe('undefined');
+ expect(stringify(undefined)).toBe('undefined');
});
test('stringify() true', () => {
- expect(stringify(true)).toBe('true');
+ expect(stringify(true)).toBe('true');
});
test('stringify() false', () => {
- expect(stringify(false)).toBe('false');
+ expect(stringify(false)).toBe('false');
});
test('stringify() a number', () => {
- expect(stringify(9000)).toBe('9000');
+ expect(stringify(9000)).toBe('9000');
});
test('stringify() a string', () => {
- expect(stringify('thing')).toBe('"thing"');
+ expect(stringify('thing')).toBe('"thing"');
});
test('stringify() a Symbol', () => {
- expect(stringify(Symbol.for('sample'))).toBe('Symbol(sample)');
+ expect(stringify(Symbol.for('sample'))).toBe('Symbol(sample)');
});
test('stringify() a RegExp', () => {
- expect(stringify(/stuff \w+/i)).toBe('/stuff \\w+/i');
+ expect(stringify(/stuff \w+/i)).toBe('/stuff \\w+/i');
});
test('stringify() an array', () => {
- expect(stringify([1, true, 'thing'])).toBe(
- dedent`
+ expect(stringify([1, true, 'thing'])).toBe(
+ dedent`
[
1,
true,
"thing",
]
`.trimEnd()
- );
+ );
});
test('stringify() nested arrays', () => {
- expect(stringify([1, true, 'thing', ['nested', null]])).toBe(
- dedent`
+ expect(stringify([1, true, 'thing', ['nested', null]])).toBe(
+ dedent`
[
1,
true,
],
]
`.trimEnd()
- );
+ );
});
test('stringify() an array with circular references', () => {
- const array: Array<any> = [1, true, 'thing'];
+ const array: Array<any> = [1, true, 'thing'];
- array.push(array);
+ array.push(array);
- expect(stringify(array)).toBe(
- dedent`
+ expect(stringify(array)).toBe(
+ dedent`
[
1,
true,
«circular»,
]
`.trimEnd()
- );
+ );
});
test('stringify() an object', () => {
- expect(stringify({a: 1, b: true})).toBe(
- dedent`
+ expect(stringify({a: 1, b: true})).toBe(
+ dedent`
{
"a": 1,
"b": true,
}
`.trimEnd()
- );
+ );
});
test('stringify() a nested object', () => {
- expect(stringify({a: 1, b: true, c: {d: null}})).toBe(
- dedent`
+ expect(stringify({a: 1, b: true, c: {d: null}})).toBe(
+ dedent`
{
"a": 1,
"b": true,
},
}
`.trimEnd()
- );
+ );
});
test('stringify() an object with circular references', () => {
- const object: {[key: string]: any} = {a: 1, b: true};
+ const object: {[key: string]: any} = {a: 1, b: true};
- object.c = object;
+ object.c = object;
- expect(stringify(object)).toBe(
- dedent`
+ expect(stringify(object)).toBe(
+ dedent`
{
"a": 1,
"b": true,
"c": «circular»,
}
`.trimEnd()
- );
+ );
});
test('stringify() a Date', () => {
- expect(stringify(new Date())).toBe('[object Date]');
+ expect(stringify(new Date())).toBe('[object Date]');
});
test('stringify() a one-line Function', () => {
- expect(stringify(() => 1)).toBe('() => 1');
+ expect(stringify(() => 1)).toBe('() => 1');
});
// @ts-ignore: suppress TS7006: Parameter 'a' implicitly has an 'any' type.
function fn(a, b) {
- if (a > 0) {
- return a + b;
- }
+ if (a > 0) {
+ return a + b;
+ }
}
test('stringify() a multi-line Function', () => {
- // Obviously this test is pretty fragile; depends on TS continuing to
- // use a 4-space indent in its build output.
- expect(stringify({fn})).toBe(
- dedent`
+ // Obviously this test is pretty fragile; depends on TS continuing to
+ // use a 4-space indent in its build output.
+ expect(stringify({fn})).toBe(
+ dedent`
{
"fn": function fn(a, b) {
if (a > 0) {
},
}
`.trimEnd()
- );
+ );
});
import {compile, fill, tokenize} from '../template';
test('compile() compiles an empty template', () => {
- expect(compile('')).toBe(dedent`
+ expect(compile('')).toBe(dedent`
let __buffer__ = "";
return __buffer__;
`);
});
test('compile() compiles a template containing only template text', () => {
- expect(compile('my stuff')).toBe(dedent`
+ expect(compile('my stuff')).toBe(dedent`
let __buffer__ = "";
__buffer__ += "my stuff";
return __buffer__;
});
test('compile() compiles a template containing an expression', () => {
- expect(compile('his name was <%= "Robert Paulson" %>')).toBe(dedent`
+ expect(compile('his name was <%= "Robert Paulson" %>')).toBe(dedent`
let __buffer__ = "";
__buffer__ += "his name was ";
__buffer__ += ("Robert Paulson");
});
test('compile() compiles a template containing statements', () => {
- expect(
- compile(dedent`
+ expect(
+ compile(dedent`
first
<% if (true) { %>
second
<% } %>
third
`)
- ).toBe(dedent`
+ ).toBe(dedent`
let __buffer__ = "";
__buffer__ += "first\\n";
if (true) {
return __buffer__;
`);
- // In practice, you'd probably use the slurping variants ("<%-", "-%>").
- expect(
- compile(dedent`
+ // In practice, you'd probably use the slurping variants ("<%-", "-%>").
+ expect(
+ compile(dedent`
first
<%- if (true) { -%>
second
<%- } -%>
third
`)
- ).toBe(dedent`
+ ).toBe(dedent`
let __buffer__ = "";
__buffer__ += "first\\n";
if (true) {
});
test('fill() fills an empty template', () => {
- expect(fill(compile(''))).toBe('');
+ expect(fill(compile(''))).toBe('');
});
test('fill() fills a template containing only template text', () => {
- expect(fill(compile('stuff'))).toBe('stuff');
+ expect(fill(compile('stuff'))).toBe('stuff');
});
test('fill() fills a template containing an expression', () => {
- expect(fill(compile('stuff <%= "here" %>'))).toBe('stuff here');
+ expect(fill(compile('stuff <%= "here" %>'))).toBe('stuff here');
});
test('fill() fills a template that relies on scope', () => {
- expect(fill(compile('name: <%= name %>'), {name: 'Bob'})).toBe('name: Bob');
+ expect(fill(compile('name: <%= name %>'), {name: 'Bob'})).toBe('name: Bob');
});
test('fill() complains when required scope is missing', () => {
- expect(() => fill(compile('name: <%= name %>'))).toThrow(
- 'name is not defined'
- );
+ expect(() => fill(compile('name: <%= name %>'))).toThrow(
+ 'name is not defined'
+ );
});
test('fill() fills a template containing statements', () => {
- expect(
- fill(
- compile(
- dedent`
+ expect(
+ fill(
+ compile(
+ dedent`
first
<% if (something === 'that') { %>
second
<% } %>
third
`
- ),
- {something: 'that'}
- )
- ).toBe(dedent`
+ ),
+ {something: 'that'}
+ )
+ ).toBe(dedent`
first
second
third
`);
- // In practice, you'd use the slurping variants ("<%-", "-%>").
- expect(
- fill(
- compile(
- dedent`
+ // In practice, you'd use the slurping variants ("<%-", "-%>").
+ expect(
+ fill(
+ compile(
+ dedent`
first
<%- if (something === 'that') { -%>
second
<%- } -%>
third
`
- ),
- {something: 'that'}
- )
- ).toBe(dedent`
+ ),
+ {something: 'that'}
+ )
+ ).toBe(dedent`
first
second
third
});
test('fill() correctly handles indented slurping delimiters', () => {
- expect(
- fill(
- compile(
- dedent`
+ expect(
+ fill(
+ compile(
+ dedent`
#start
<%- if (something === 'that') { -%>
middle
<%- } -%>
#end
`
- ),
- {something: 'that'}
- )
- ).toBe(dedent`
+ ),
+ {something: 'that'}
+ )
+ ).toBe(dedent`
#start
middle
#end
});
test('fill() correctly handles slurping delimiters at edges of template', () => {
- expect(
- fill(
- compile(
- dedent`
+ expect(
+ fill(
+ compile(
+ dedent`
<%- if (something === 'that') { -%>
conditional
<%- } -%>
`
- ),
- {something: 'that'}
- )
- ).toBe(dedent`
+ ),
+ {something: 'that'}
+ )
+ ).toBe(dedent`
conditional
`);
});
test('tokenize() handles empty input', () => {
- expect([...tokenize('')]).toEqual([]);
+ expect([...tokenize('')]).toEqual([]);
});
test('tokenize() handles a template containing only template text', () => {
- expect([...tokenize('an example here')]).toEqual([
- {
- kind: 'TemplateText',
- text: 'an example here',
- },
- ]);
+ expect([...tokenize('an example here')]).toEqual([
+ {
+ kind: 'TemplateText',
+ text: 'an example here',
+ },
+ ]);
});
test('tokenize() handles a template containing an expression', () => {
- expect([...tokenize('this <%= "thing" %>')]).toEqual([
- {kind: 'TemplateText', text: 'this '},
- {kind: 'StartExpression'},
- {kind: 'HostText', text: ' "thing" '},
- {kind: 'EndDelimiter'},
- ]);
+ expect([...tokenize('this <%= "thing" %>')]).toEqual([
+ {kind: 'TemplateText', text: 'this '},
+ {kind: 'StartExpression'},
+ {kind: 'HostText', text: ' "thing" '},
+ {kind: 'EndDelimiter'},
+ ]);
});
test('tokenize() handles a template containing a statement', () => {
- expect([...tokenize('this <% call() %>')]).toEqual([
- {kind: 'TemplateText', text: 'this '},
- {kind: 'StartStatement'},
- {kind: 'HostText', text: ' call() '},
- {kind: 'EndDelimiter'},
- ]);
+ expect([...tokenize('this <% call() %>')]).toEqual([
+ {kind: 'TemplateText', text: 'this '},
+ {kind: 'StartStatement'},
+ {kind: 'HostText', text: ' call() '},
+ {kind: 'EndDelimiter'},
+ ]);
});
test('tokenize() eats a newline after a "-%>" delimiter', () => {
- expect([...tokenize('before\n<% something -%>\nafter')]).toEqual([
- {kind: 'TemplateText', text: 'before\n'},
- {kind: 'StartStatement'},
- {kind: 'HostText', text: ' something '},
- {kind: 'EndDelimiter'},
- {kind: 'TemplateText', text: 'after'},
- ]);
-
- expect([...tokenize('before\n<%= something -%>\nafter')]).toEqual([
- {kind: 'TemplateText', text: 'before\n'},
- {kind: 'StartExpression'},
- {kind: 'HostText', text: ' something '},
- {kind: 'EndDelimiter'},
- {kind: 'TemplateText', text: 'after'},
- ]);
+ expect([...tokenize('before\n<% something -%>\nafter')]).toEqual([
+ {kind: 'TemplateText', text: 'before\n'},
+ {kind: 'StartStatement'},
+ {kind: 'HostText', text: ' something '},
+ {kind: 'EndDelimiter'},
+ {kind: 'TemplateText', text: 'after'},
+ ]);
+
+ expect([...tokenize('before\n<%= something -%>\nafter')]).toEqual([
+ {kind: 'TemplateText', text: 'before\n'},
+ {kind: 'StartExpression'},
+ {kind: 'HostText', text: ' something '},
+ {kind: 'EndDelimiter'},
+ {kind: 'TemplateText', text: 'after'},
+ ]);
});
test('tokenize() eats whitespace between previous newline and "<%-" delimiter', () => {
- expect([...tokenize('before\n <%- something %>\nafter')]).toEqual([
- {kind: 'TemplateText', text: 'before\n'},
- {kind: 'StartStatement'},
- {kind: 'HostText', text: ' something '},
- {kind: 'EndDelimiter'},
- {kind: 'TemplateText', text: '\nafter'},
- ]);
-
- // But note that, more realistically, "<%-" and "-%>" are generally used
- // together.
- expect([...tokenize('before\n <%- something -%>\nafter')]).toEqual([
- {kind: 'TemplateText', text: 'before\n'},
- {kind: 'StartStatement'},
- {kind: 'HostText', text: ' something '},
- {kind: 'EndDelimiter'},
- {kind: 'TemplateText', text: 'after'},
- ]);
+ expect([...tokenize('before\n <%- something %>\nafter')]).toEqual([
+ {kind: 'TemplateText', text: 'before\n'},
+ {kind: 'StartStatement'},
+ {kind: 'HostText', text: ' something '},
+ {kind: 'EndDelimiter'},
+ {kind: 'TemplateText', text: '\nafter'},
+ ]);
+
+ // But note that, more realistically, "<%-" and "-%>" are generally used
+ // together.
+ expect([...tokenize('before\n <%- something -%>\nafter')]).toEqual([
+ {kind: 'TemplateText', text: 'before\n'},
+ {kind: 'StartStatement'},
+ {kind: 'HostText', text: ' something '},
+ {kind: 'EndDelimiter'},
+ {kind: 'TemplateText', text: 'after'},
+ ]);
});
test('tokenize() complains about unexpected start delimiters', () => {
- expect(() => [...tokenize('outer <% inner <%')]).toThrow(
- 'Unexpected start delimiter "<%" at index 15'
- );
+ expect(() => [...tokenize('outer <% inner <%')]).toThrow(
+ 'Unexpected start delimiter "<%" at index 15'
+ );
- expect(() => [...tokenize('outer <%= inner <%')]).toThrow(
- 'Unexpected start delimiter "<%" at index 16'
- );
+ expect(() => [...tokenize('outer <%= inner <%')]).toThrow(
+ 'Unexpected start delimiter "<%" at index 16'
+ );
});
test('tokenize() complains about unexpected end delimiters', () => {
- expect(() => [...tokenize('before %>')]).toThrow(
- 'Unexpected end delimiter "%>" at index 7'
- );
+ expect(() => [...tokenize('before %>')]).toThrow(
+ 'Unexpected end delimiter "%>" at index 7'
+ );
});
* sense).
*/
export default function assert(
- condition: any,
- message?: string
+ condition: any,
+ message?: string
): asserts condition {
- if (!condition) {
- throw new Error(`assert(): ${message || 'assertion failed'}`);
- }
+ if (!condition) {
+ throw new Error(`assert(): ${message || 'assertion failed'}`);
+ }
}
import run from './run';
type Options = {
- group?: string;
- sudo?: boolean;
- user?: string;
+ group?: string;
+ sudo?: boolean;
+ user?: string;
};
export default async function chown(
- path: string,
- options: Options = {}
+ path: string,
+ options: Options = {}
): Promise<Error | null> {
- if (Context.attributes.platform === 'darwin') {
- return null; // TODO finish
- } else {
- throw new Error('TODO: implement');
- }
+ if (Context.attributes.platform === 'darwin') {
+ return null; // TODO finish
+ } else {
+ throw new Error('TODO: implement');
+ }
}
// TODO: decide whether to throw/catch or just return errors
* @overload
*/
function bold(
- input: TemplateStringsArray,
- ...interpolations: unknown[]
+ input: TemplateStringsArray,
+ ...interpolations: unknown[]
): string;
function bold(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), BOLD);
- } else {
- return style(input, BOLD);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), BOLD);
+ } else {
+ return style(input, BOLD);
+ }
}
/**
* @overload
*/
function green(
- input: TemplateStringsArray,
- ...interpolations: unknown[]
+ input: TemplateStringsArray,
+ ...interpolations: unknown[]
): string;
function green(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), GREEN);
- } else {
- return style(input, GREEN);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), GREEN);
+ } else {
+ return style(input, GREEN);
+ }
}
/**
* @overload
*/
function purple(
- input: TemplateStringsArray,
- ...interpolations: unknown[]
+ input: TemplateStringsArray,
+ ...interpolations: unknown[]
): string;
function purple(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), PURPLE);
- } else {
- return style(input, PURPLE);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), PURPLE);
+ } else {
+ return style(input, PURPLE);
+ }
}
/**
function red(input: TemplateStringsArray, ...interpolations: unknown[]): string;
function red(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), RED);
- } else {
- return style(input, RED);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), RED);
+ } else {
+ return style(input, RED);
+ }
}
/**
* @overload
*/
function reverse(
- input: TemplateStringsArray,
- ...interpolations: unknown[]
+ input: TemplateStringsArray,
+ ...interpolations: unknown[]
): string;
function reverse(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), REVERSE);
- } else {
- return style(input, REVERSE);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), REVERSE);
+ } else {
+ return style(input, REVERSE);
+ }
}
/**
* @overload
*/
function yellow(
- input: TemplateStringsArray,
- ...interpolations: unknown[]
+ input: TemplateStringsArray,
+ ...interpolations: unknown[]
): string;
function yellow(input: any, ...interpolations: unknown[]) {
- if (Array.isArray(input)) {
- return style(interpolate(input as any, interpolations), YELLOW);
- } else {
- return style(input, YELLOW);
- }
+ if (Array.isArray(input)) {
+ return style(interpolate(input as any, interpolations), YELLOW);
+ } else {
+ return style(input, YELLOW);
+ }
}
function style(text: string, escape: string) {
- return `${escape}${text}${RESET}`;
+ return `${escape}${text}${RESET}`;
}
function interpolate(strings: TemplateStringsArray, interpolations: unknown[]) {
- return strings.reduce((acc, string, i) => {
- if (i < interpolations.length) {
- return acc + string + String(interpolations[i]);
- } else {
- return acc + string;
- }
- }, '');
+ return strings.reduce((acc, string, i) => {
+ if (i < interpolations.length) {
+ return acc + string + String(interpolations[i]);
+ } else {
+ return acc + string;
+ }
+ }, '');
}
const COLORS = {
- bold(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = bold(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
-
- green(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = green(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
-
- purple(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = purple(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
-
- red(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = red(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
-
- reverse(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = reverse(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
-
- yellow(
- this: unknown,
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
- ): string {
- const result = yellow(strings, ...interpolations);
-
- if (typeof this === 'function') {
- return this.call(null, result);
- } else {
- return result;
- }
- },
+ bold(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = bold(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
+
+ green(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = green(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
+
+ purple(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = purple(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
+
+ red(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = red(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
+
+ reverse(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = reverse(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
+
+ yellow(
+ this: unknown,
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
+ ): string {
+ const result = yellow(strings, ...interpolations);
+
+ if (typeof this === 'function') {
+ return this.call(null, result);
+ } else {
+ return result;
+ }
+ },
};
export default {
- bold: Object.assign(COLORS.bold, COLORS),
- green: Object.assign(COLORS.green, COLORS),
- purple: Object.assign(COLORS.purple, COLORS),
- red: Object.assign(COLORS.red, COLORS),
- reverse: Object.assign(COLORS.reverse, COLORS),
- yellow: Object.assign(COLORS.yellow, COLORS),
+ bold: Object.assign(COLORS.bold, COLORS),
+ green: Object.assign(COLORS.green, COLORS),
+ purple: Object.assign(COLORS.purple, COLORS),
+ red: Object.assign(COLORS.red, COLORS),
+ reverse: Object.assign(COLORS.reverse, COLORS),
+ yellow: Object.assign(COLORS.yellow, COLORS),
};
const PREFIXES = ['debug', 'error', 'info', 'notice', 'warning'];
const PREFIX_LENGTH = PREFIXES.reduce((acc, prefix) => {
- return Math.max(`[${prefix}] `.length, acc);
+ return Math.max(`[${prefix}] `.length, acc);
}, 0);
const PREFIX_MAP = Object.fromEntries(
- PREFIXES.map((prefix) => {
- return [prefix, `[${prefix}] `.padEnd(PREFIX_LENGTH)];
- })
+ PREFIXES.map((prefix) => {
+ return [prefix, `[${prefix}] `.padEnd(PREFIX_LENGTH)];
+ })
);
/**
* @see https://en.wikipedia.org/wiki/Syslog
*/
export const LOG_LEVEL = {
- EMERGENCY: 0,
- ALERT: 1,
- CRITICAL: 2,
- ERROR: 3,
- WARNING: 4,
- NOTICE: 5,
- INFO: 6,
- DEBUG: 7,
+ EMERGENCY: 0,
+ ALERT: 1,
+ CRITICAL: 2,
+ ERROR: 3,
+ WARNING: 4,
+ NOTICE: 5,
+ INFO: 6,
+ DEBUG: 7,
} as const;
export function clear() {
- return new Promise((resolve) => {
- clearLine(process.stderr, 0, () => {
- cursorTo(process.stderr, 0, undefined, resolve);
+ return new Promise((resolve) => {
+ clearLine(process.stderr, 0, () => {
+ cursorTo(process.stderr, 0, undefined, resolve);
+ });
});
- });
}
/**
* arguments to `log.debug()`.
*/
export async function debug(callback: () => unknown): Promise<void> {
- if (logLevel >= LOG_LEVEL.DEBUG) {
- await callback();
- }
+ if (logLevel >= LOG_LEVEL.DEBUG) {
+ await callback();
+ }
}
export function getLogLevel(): LogLevel {
- return logLevel;
+ return logLevel;
}
export function log(...args: Array<any>) {
- print(...args);
- print('\n');
+ print(...args);
+ print('\n');
}
log.debug = function debug(message: string) {
- if (logLevel >= LOG_LEVEL.DEBUG) {
- log(purple.bold`${PREFIX_MAP.debug}` + message);
- }
+ if (logLevel >= LOG_LEVEL.DEBUG) {
+ log(purple.bold`${PREFIX_MAP.debug}` + message);
+ }
};
log.error = function error(message: string) {
- if (logLevel >= LOG_LEVEL.ERROR) {
- log(red.bold`${PREFIX_MAP.error}` + message);
- }
+ if (logLevel >= LOG_LEVEL.ERROR) {
+ log(red.bold`${PREFIX_MAP.error}` + message);
+ }
};
log.info = function info(message: string) {
- if (logLevel >= LOG_LEVEL.INFO) {
- log(bold`${PREFIX_MAP.info}` + message);
- }
+ if (logLevel >= LOG_LEVEL.INFO) {
+ log(bold`${PREFIX_MAP.info}` + message);
+ }
};
log.notice = function notice(message: string) {
- if (logLevel >= LOG_LEVEL.NOTICE) {
- log(green.bold`${PREFIX_MAP.notice}` + message);
- }
+ if (logLevel >= LOG_LEVEL.NOTICE) {
+ log(green.bold`${PREFIX_MAP.notice}` + message);
+ }
};
log.warn = function warn(message: string) {
- if (logLevel >= LOG_LEVEL.WARNING) {
- log(yellow.bold`${PREFIX_MAP.warning}` + message);
- }
+ if (logLevel >= LOG_LEVEL.WARNING) {
+ log(yellow.bold`${PREFIX_MAP.warning}` + message);
+ }
};
export function print(...args: Array<any>) {
- process.stderr.write(
- args
- .map((arg) => {
- try {
- if (typeof arg === 'object' && arg) {
- return JSON.stringify(arg, null, 2);
- } else {
- return String(arg);
- }
- } catch {
- return '???';
- }
- })
- .join(' ')
- );
+ process.stderr.write(
+ args
+ .map((arg) => {
+ try {
+ if (typeof arg === 'object' && arg) {
+ return JSON.stringify(arg, null, 2);
+ } else {
+ return String(arg);
+ }
+ } catch {
+ return '???';
+ }
+ })
+ .join(' ')
+ );
}
export function setLogLevel(level: LogLevel) {
- logLevel = level;
+ logLevel = level;
}
log.clear = clear;
export default function dedent(
- strings: TemplateStringsArray,
- ...interpolations: unknown[]
+ strings: TemplateStringsArray,
+ ...interpolations: unknown[]
): string {
- // Insert interpolations in template.
- const text: string = strings.reduce((acc, string, i) => {
- if (i < interpolations.length) {
- return acc + string + String(interpolations[i]);
- } else {
- return acc + string;
- }
- }, '');
+ // Insert interpolations in template.
+ const text: string = strings.reduce((acc, string, i) => {
+ if (i < interpolations.length) {
+ return acc + string + String(interpolations[i]);
+ } else {
+ return acc + string;
+ }
+ }, '');
- // Collapse totally blank lines to empty strings.
- const lines = text.split(/\r\n?|\n/).map((line: string) => {
- if (line.match(/^\s+$/)) {
- return '';
- } else {
- return line;
- }
- });
+ // Collapse totally blank lines to empty strings.
+ const lines = text.split(/\r\n?|\n/).map((line: string) => {
+ if (line.match(/^\s+$/)) {
+ return '';
+ } else {
+ return line;
+ }
+ });
- // Find minimum indent (ignoring empty lines).
- const minimum = lines.reduce((acc: number, line: string) => {
- const indent = line.match(/^\s+/);
- if (indent) {
- const length = indent[0].length;
- return Math.min(acc, length);
- }
- return acc;
- }, Infinity);
+ // Find minimum indent (ignoring empty lines).
+ const minimum = lines.reduce((acc: number, line: string) => {
+ const indent = line.match(/^\s+/);
+ if (indent) {
+ const length = indent[0].length;
+ return Math.min(acc, length);
+ }
+ return acc;
+ }, Infinity);
- // Strip out minimum indent from every line.
- const dedented = isFinite(minimum)
- ? lines.map((line: string) =>
- line.replace(new RegExp(`^${' '.repeat(minimum)}`, 'g'), '')
- )
- : lines;
+ // Strip out minimum indent from every line.
+ const dedented = isFinite(minimum)
+ ? lines.map((line: string) =>
+ line.replace(new RegExp(`^${' '.repeat(minimum)}`, 'g'), '')
+ )
+ : lines;
- // Trim first and last line if empty.
- if (dedented[0] === '') {
- dedented.shift();
- }
+ // Trim first and last line if empty.
+ if (dedented[0] === '') {
+ dedented.shift();
+ }
- if (dedented[dedented.length - 1] === '') {
- dedented.pop();
- }
+ if (dedented[dedented.length - 1] === '') {
+ dedented.pop();
+ }
- return dedented.join('\n') + '\n';
+ return dedented.join('\n') + '\n';
}
export default function escapeRegExpPattern(pattern: string): string {
- // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
- return pattern.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+ return pattern.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
}
import {join} from 'path';
export default function expand(path: string) {
- if (path.startsWith('~/')) {
- return join(homedir(), path.slice(2));
- } else {
- return path;
- }
+ if (path.startsWith('~/')) {
+ return join(homedir(), path.slice(2));
+ } else {
+ return path;
+ }
}
* @see https://v8.dev/docs/stack-trace-api
*/
export default function getCaller(): string {
- let name;
+ let name;
- const prepareStackTrace = Error.prepareStackTrace;
+ const prepareStackTrace = Error.prepareStackTrace;
- try {
- Error.prepareStackTrace = (
- _error: Error,
- callsites: Array<NodeJS.CallSite>
- ): Array<NodeJS.CallSite> => {
- return callsites;
- };
+ try {
+ Error.prepareStackTrace = (
+ _error: Error,
+ callsites: Array<NodeJS.CallSite>
+ ): Array<NodeJS.CallSite> => {
+ return callsites;
+ };
- const stack: Array<NodeJS.CallSite> = new Error().stack as any;
+ const stack: Array<NodeJS.CallSite> = new Error().stack as any;
- // Skip two stack frames (this function, and caller of this
- // function), to get caller of our caller.
- name = stack.length > 2 ? stack[2].getFileName() : '';
- } finally {
- Error.prepareStackTrace = prepareStackTrace;
- }
+ // Skip two stack frames (this function, and caller of this
+ // function), to get caller of our caller.
+ name = stack.length > 2 ? stack[2].getFileName() : '';
+ } finally {
+ Error.prepareStackTrace = prepareStackTrace;
+ }
- return name || '[unknown]';
+ return name || '[unknown]';
}
import type {Aspect} from './types/Project';
type Options = {
- focused: Set<Aspect>;
- logLevel: LogLevel;
- startAt: {
- found: boolean;
- literal: string;
- fuzzy?: RegExp;
- };
- testsOnly: boolean;
+ focused: Set<Aspect>;
+ logLevel: LogLevel;
+ startAt: {
+ found: boolean;
+ literal: string;
+ fuzzy?: RegExp;
+ };
+ testsOnly: boolean;
};
const {bold} = COLORS;
export default async function getOptions(
- args: Array<string>
+ args: Array<string>
): Promise<Options> {
- const options: Options = {
- focused: new Set(),
- logLevel: LOG_LEVEL.INFO,
- startAt: {
- found: false,
- literal: '',
- },
- testsOnly: false,
- };
+ const options: Options = {
+ focused: new Set(),
+ logLevel: LOG_LEVEL.INFO,
+ startAt: {
+ found: false,
+ literal: '',
+ },
+ testsOnly: false,
+ };
- const directory = path.join(root, 'aspects');
+ const directory = path.join(root, 'aspects');
- const entries = await fs.readdir(directory, {withFileTypes: true});
+ const entries = await fs.readdir(directory, {withFileTypes: true});
- const aspects: Array<[string, string]> = [];
+ const aspects: Array<[string, string]> = [];
- for (const entry of entries) {
- if (entry.isDirectory()) {
- const name = entry.name;
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const name = entry.name;
- const {description} = await readAspect(
- path.join(directory, name, 'aspect.json')
- );
+ const {description} = await readAspect(
+ path.join(directory, name, 'aspect.json')
+ );
- aspects.push([name, description]);
+ aspects.push([name, description]);
+ }
}
- }
-
- for (const arg of args) {
- if (arg === '--debug' || arg === '-d') {
- options.logLevel = LOG_LEVEL.DEBUG;
- } else if (arg === '--quiet' || arg === '-q') {
- options.logLevel = LOG_LEVEL.NOTICE;
- } else if (arg === '--test' || arg === '-t') {
- options.testsOnly = true;
- } else if (arg === '--help' || arg === '-h') {
- await printUsage(aspects);
- throw new ErrorWithMetadata('aborting');
- } else if (arg.startsWith('--start-at-task=')) {
- options.startAt.literal = (
- arg.match(/^--start-at-task=(.*)/)?.[1] ?? ''
- ).trim();
- options.startAt.fuzzy = new RegExp(
- [
- '',
- ...options.startAt.literal.split(/\s+/).map(escapeRegExpPattern),
- '',
- ].join('.*'),
- 'i'
- );
- } else if (arg.startsWith('-')) {
- throw new ErrorWithMetadata(
- `unrecognized argument ${JSON.stringify(
- arg
- )} - pass "--help" to see allowed options`
- );
- } else {
- try {
- assertAspect(arg);
- options.focused.add(arg);
- } catch {
- throw new ErrorWithMetadata(
- `unrecognized aspect ${JSON.stringify(
- arg
- )} - pass "--help" to see full list`
- );
- }
+
+ for (const arg of args) {
+ if (arg === '--debug' || arg === '-d') {
+ options.logLevel = LOG_LEVEL.DEBUG;
+ } else if (arg === '--quiet' || arg === '-q') {
+ options.logLevel = LOG_LEVEL.NOTICE;
+ } else if (arg === '--test' || arg === '-t') {
+ options.testsOnly = true;
+ } else if (arg === '--help' || arg === '-h') {
+ await printUsage(aspects);
+ throw new ErrorWithMetadata('aborting');
+ } else if (arg.startsWith('--start-at-task=')) {
+ options.startAt.literal = (
+ arg.match(/^--start-at-task=(.*)/)?.[1] ?? ''
+ ).trim();
+ options.startAt.fuzzy = new RegExp(
+ [
+ '',
+ ...options.startAt.literal
+ .split(/\s+/)
+ .map(escapeRegExpPattern),
+ '',
+ ].join('.*'),
+ 'i'
+ );
+ } else if (arg.startsWith('-')) {
+ throw new ErrorWithMetadata(
+ `unrecognized argument ${JSON.stringify(
+ arg
+ )} - pass "--help" to see allowed options`
+ );
+ } else {
+ try {
+ assertAspect(arg);
+ options.focused.add(arg);
+ } catch {
+ throw new ErrorWithMetadata(
+ `unrecognized aspect ${JSON.stringify(
+ arg
+ )} - pass "--help" to see full list`
+ );
+ }
+ }
}
- }
- return options;
+ return options;
}
async function printUsage(aspects: Array<[string, string]>) {
- // TODO: actually implement all the switches mentioned here
- log(
- dedent`
+ // TODO: actually implement all the switches mentioned here
+ log(
+ dedent`
./install [options] [aspects...]
${bold`Aspects:`}
`
- );
+ );
- for (const [aspect, description] of aspects) {
- log(` ${aspect}`);
- log(` ${description}`);
- }
+ for (const [aspect, description] of aspects) {
+ log(` ${aspect}`);
+ log(` ${description}`);
+ }
- log();
+ log();
}
import test from './test';
async function main() {
- if (Context.attributes.uid === 0) {
- throw new ErrorWithMetadata('Cannot run as root');
- }
-
- // Skip first two args (node executable and main.js script).
- const options = await getOptions(process.argv.slice(2));
-
- setLogLevel(options.logLevel);
-
- debug(() => {
- log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
- log.debug('getOptions():\n\n' + stringify(options) + '\n');
- });
-
- if (process.cwd() === root) {
- log.info(`Working from root: ${simplify(root)}`);
- } else {
- log.notice(`Changing to root: ${simplify(root)}`);
- process.chdir(root);
- }
-
- log.info('Running tests');
-
- await test();
-
- if (options.testsOnly) {
- return;
- }
-
- const project = await readProject(path.join(root, 'project.json'));
-
- const hostname = os.hostname();
-
- const profiles = project.profiles ?? {};
-
- const [profile] =
- Object.entries(profiles).find(([, {pattern}]) => {
- if (regExpFromString(pattern).test(hostname)) {
- return true;
- }
- }) || [];
+ if (Context.attributes.uid === 0) {
+ throw new ErrorWithMetadata('Cannot run as root');
+ }
- log.info(`Profile: ${profile || 'n/a'}`);
+ // Skip first two args (node executable and main.js script).
+ const options = await getOptions(process.argv.slice(2));
- const profileVariables: {[key: string]: JSONValue} = profile
- ? profiles[profile]!.variables ?? {}
- : {};
+ setLogLevel(options.logLevel);
- const platform = Context.attributes.platform;
+ debug(() => {
+ log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
+ log.debug('getOptions():\n\n' + stringify(options) + '\n');
+ });
- log.info(`Platform: ${platform}`);
+ if (process.cwd() === root) {
+ log.info(`Working from root: ${simplify(root)}`);
+ } else {
+ log.notice(`Changing to root: ${simplify(root)}`);
+ process.chdir(root);
+ }
- const {aspects, variables: platformVariables = {}} = project.platforms[
- platform
- ];
+ log.info('Running tests');
- // Register tasks.
- const candidateTasks = [];
+ await test();
- for (const aspect of aspects) {
- switch (aspect) {
- case 'launchd':
- require('../aspects/launchd');
- break;
- case 'terminfo':
- require('../aspects/terminfo');
- break;
+ if (options.testsOnly) {
+ return;
}
- // Check for an exact match of the starting task if `--start-at-task=` was
- // supplied.
- for (const [, name] of Context.tasks.get(aspect)) {
- if (name === options.startAt.literal) {
- options.startAt.found = true;
- } else if (
- !options.startAt.found &&
- options.startAt.fuzzy &&
- options.startAt.fuzzy.test(name)
- ) {
- candidateTasks.push(name);
- }
- }
- }
+ const project = await readProject(path.join(root, 'project.json'));
- if (!options.startAt.found && candidateTasks.length === 1) {
- log.notice(`Matching task found: ${candidateTasks[0]}`);
+ const hostname = os.hostname();
- log();
+ const profiles = project.profiles ?? {};
- const reply = await prompt('Start running at this task? [y/n]: ');
+ const [profile] =
+ Object.entries(profiles).find(([, {pattern}]) => {
+ if (regExpFromString(pattern).test(hostname)) {
+ return true;
+ }
+ }) || [];
- if (/^\s*y(?:e(?:s)?)?\s*$/i.test(reply)) {
- options.startAt.found = true;
- options.startAt.literal = candidateTasks[0];
- } else {
- throw new ErrorWithMetadata('User aborted');
- }
- } else if (!options.startAt.found && candidateTasks.length > 1) {
- log.notice(`${candidateTasks.length} tasks found:\n`);
+ log.info(`Profile: ${profile || 'n/a'}`);
- const width = candidateTasks.length.toString().length;
+ const profileVariables: {[key: string]: JSONValue} = profile
+ ? profiles[profile]!.variables ?? {}
+ : {};
- while (!options.startAt.found) {
- candidateTasks.forEach((name, i) => {
- log(`${(i + 1).toString().padStart(width)}: ${name}`);
- });
+ const platform = Context.attributes.platform;
- log();
+ log.info(`Platform: ${platform}`);
- const reply = await prompt('Start at task number: ');
+ const {aspects, variables: platformVariables = {}} = project.platforms[
+ platform
+ ];
- const choice = parseInt(reply.trim(), 10);
+ // Register tasks.
+ const candidateTasks = [];
- if (
- Number.isNaN(choice) ||
- choice < 1 ||
- choice > candidateTasks.length
- ) {
- log.warn(
- `Invalid choice ${JSON.stringify(
- reply
- )}; try again or press CTRL-C to abort.`
- );
+ for (const aspect of aspects) {
+ switch (aspect) {
+ case 'launchd':
+ require('../aspects/launchd');
+ break;
+ case 'terminfo':
+ require('../aspects/terminfo');
+ break;
+ }
- log();
- } else {
- options.startAt.found = true;
- options.startAt.literal = candidateTasks[choice - 1];
- }
+ // Check for an exact match of the starting task if `--start-at-task=` was
+ // supplied.
+ for (const [, name] of Context.tasks.get(aspect)) {
+ if (name === options.startAt.literal) {
+ options.startAt.found = true;
+ } else if (
+ !options.startAt.found &&
+ options.startAt.fuzzy &&
+ options.startAt.fuzzy.test(name)
+ ) {
+ candidateTasks.push(name);
+ }
+ }
}
- }
-
- const baseVariables = merge(profileVariables, platformVariables);
- // Execute tasks.
- try {
- for (const aspect of aspects) {
- const {variables: aspectVariables = {}} = await readAspect(
- path.join(root, 'aspects', aspect, 'aspect.json')
- );
-
- if (options.focused.size && !options.focused.has(aspect)) {
- log.info(`Skipping aspect: ${aspect}`);
- continue;
- }
+ if (!options.startAt.found && candidateTasks.length === 1) {
+ log.notice(`Matching task found: ${candidateTasks[0]}`);
- const variables = merge(aspectVariables, baseVariables);
+ log();
- // log.debug(`variables:\n\n${JSON.stringify(variables, null, 2)}\n`);
+ const reply = await prompt('Start running at this task? [y/n]: ');
- for (const [callback, name] of Context.tasks.get(aspect)) {
- if (!options.startAt.found || name === options.startAt.literal) {
- options.startAt.found = false;
- log.info(`Task: ${name}`);
+ if (/^\s*y(?:e(?:s)?)?\s*$/i.test(reply)) {
+ options.startAt.found = true;
+ options.startAt.literal = candidateTasks[0];
+ } else {
+ throw new ErrorWithMetadata('User aborted');
+ }
+ } else if (!options.startAt.found && candidateTasks.length > 1) {
+ log.notice(`${candidateTasks.length} tasks found:\n`);
+
+ const width = candidateTasks.length.toString().length;
+
+ while (!options.startAt.found) {
+ candidateTasks.forEach((name, i) => {
+ log(`${(i + 1).toString().padStart(width)}: ${name}`);
+ });
+
+ log();
+
+ const reply = await prompt('Start at task number: ');
+
+ const choice = parseInt(reply.trim(), 10);
+
+ if (
+ Number.isNaN(choice) ||
+ choice < 1 ||
+ choice > candidateTasks.length
+ ) {
+ log.warn(
+ `Invalid choice ${JSON.stringify(
+ reply
+ )}; try again or press CTRL-C to abort.`
+ );
+
+ log();
+ } else {
+ options.startAt.found = true;
+ options.startAt.literal = candidateTasks[choice - 1];
+ }
+ }
+ }
- await Context.withContext({aspect, variables}, async () => {
- await callback();
- });
+ const baseVariables = merge(profileVariables, platformVariables);
+
+ // Execute tasks.
+ try {
+ for (const aspect of aspects) {
+ const {variables: aspectVariables = {}} = await readAspect(
+ path.join(root, 'aspects', aspect, 'aspect.json')
+ );
+
+ if (options.focused.size && !options.focused.has(aspect)) {
+ log.info(`Skipping aspect: ${aspect}`);
+ continue;
+ }
+
+ const variables = merge(aspectVariables, baseVariables);
+
+ // log.debug(`variables:\n\n${JSON.stringify(variables, null, 2)}\n`);
+
+ for (const [callback, name] of Context.tasks.get(aspect)) {
+ if (
+ !options.startAt.found ||
+ name === options.startAt.literal
+ ) {
+ options.startAt.found = false;
+ log.info(`Task: ${name}`);
+
+ await Context.withContext({aspect, variables}, async () => {
+ await callback();
+ });
+ }
+ }
}
- }
+ } finally {
+ const counts = Object.entries(Context.counts)
+ .map(([name, count]) => {
+ return `${name}=${count}`;
+ })
+ .join(' ');
+
+ log.info(`Summary: ${counts}`);
}
- } finally {
- const counts = Object.entries(Context.counts)
- .map(([name, count]) => {
- return `${name}=${count}`;
- })
- .join(' ');
-
- log.info(`Summary: ${counts}`);
- }
}
main().catch((error) => {
- if (error instanceof ErrorWithMetadata) {
- if (error.metadata) {
- log.error(
- `${error.message}\n\n${JSON.stringify(error.metadata, null, 2)}\n`
- );
+ if (error instanceof ErrorWithMetadata) {
+ if (error.metadata) {
+ log.error(
+ `${error.message}\n\n${JSON.stringify(
+ error.metadata,
+ null,
+ 2
+ )}\n`
+ );
+ } else {
+ log.error(error.message);
+ }
} else {
- log.error(error.message);
+ log.error(error.toString());
}
- } else {
- log.error(error.toString());
- }
- process.exit(1);
+ process.exit(1);
});
type Variables = {
- [key: string]: JSONValue;
+ [key: string]: JSONValue;
};
export default function merge(
- variables: Variables,
- ...rest: Array<Variables>
+ variables: Variables,
+ ...rest: Array<Variables>
): Variables {
- if (!rest.length) {
- return variables;
- } else if (rest.length === 1) {
- return mergeObjects(variables, rest[0]);
- } else {
- const last = rest.pop()!;
- const penultimate = rest.pop()!;
+ if (!rest.length) {
+ return variables;
+ } else if (rest.length === 1) {
+ return mergeObjects(variables, rest[0]);
+ } else {
+ const last = rest.pop()!;
+ const penultimate = rest.pop()!;
- return merge(variables, ...rest.concat(merge(penultimate, last)));
- }
+ return merge(variables, ...rest.concat(merge(penultimate, last)));
+ }
}
function mergeObjects(target: Variables, source: Variables): Variables {
- const output: Variables = {...target};
+ const output: Variables = {...target};
- Object.entries(source).forEach(([key, value]) => {
- if (
- value &&
- typeof value === 'object' &&
- !Array.isArray(value) &&
- target[key] &&
- typeof target[key] === 'object' &&
- !Array.isArray(target[key])
- ) {
- output[key] = mergeObjects(target[key] as Variables, value as Variables);
- } else {
- output[key] = value;
- }
- });
+ Object.entries(source).forEach(([key, value]) => {
+ if (
+ value &&
+ typeof value === 'object' &&
+ !Array.isArray(value) &&
+ target[key] &&
+ typeof target[key] === 'object' &&
+ !Array.isArray(target[key])
+ ) {
+ output[key] = mergeObjects(
+ target[key] as Variables,
+ value as Variables
+ );
+ } else {
+ output[key] = value;
+ }
+ });
- return output;
+ return output;
}
import {Writable} from 'stream';
type Options = {
- private?: boolean;
+ private?: boolean;
};
export default async function prompt(
- text: string,
- options: Options = {}
+ text: string,
+ options: Options = {}
): Promise<string> {
- let muted = false;
+ let muted = false;
- // https://stackoverflow.com/a/33500118/2103996
- const stdout = new Writable({
- write: (chunk, _encoding, callback) => {
- if (!muted) {
- process.stdout.write(chunk);
- }
- callback();
- },
- });
-
- const rl = readline.createInterface({
- historySize: 0,
- input: process.stdin,
- output: stdout,
- terminal: true,
- });
+ // https://stackoverflow.com/a/33500118/2103996
+ const stdout = new Writable({
+ write: (chunk, _encoding, callback) => {
+ if (!muted) {
+ process.stdout.write(chunk);
+ }
+ callback();
+ },
+ });
- try {
- const response = new Promise<string>((resolve) => {
- rl.question(text, (response) => {
- process.stdout.write('\n');
- resolve(response);
- });
+ const rl = readline.createInterface({
+ historySize: 0,
+ input: process.stdin,
+ output: stdout,
+ terminal: true,
});
- muted = !!options.private;
+ try {
+ const response = new Promise<string>((resolve) => {
+ rl.question(text, (response) => {
+ process.stdout.write('\n');
+ resolve(response);
+ });
+ });
+
+ muted = !!options.private;
- return await response;
- } finally {
- rl.close();
- }
+ return await response;
+ } finally {
+ rl.close();
+ }
}
const readFile = promisify(fs.readFile);
export default async function readAspect(path: string): Promise<Aspect> {
- log.debug(`Reading aspect configuration: ${path}`);
+ log.debug(`Reading aspect configuration: ${path}`);
- const json = await readFile(path, 'utf8');
+ const json = await readFile(path, 'utf8');
- const aspect = JSON.parse(json);
+ const aspect = JSON.parse(json);
- try {
- assertAspect(aspect);
- } catch (error) {
- throw new Error(`${error.message} in ${path}`);
- }
+ try {
+ assertAspect(aspect);
+ } catch (error) {
+ throw new Error(`${error.message} in ${path}`);
+ }
- return aspect;
+ return aspect;
}
const readFile = promisify(fs.readFile);
export default async function readProject(path: string): Promise<Project> {
- log.debug(`Reading project configuration: ${path}`);
+ log.debug(`Reading project configuration: ${path}`);
- const json = await readFile(path, 'utf8');
+ const json = await readFile(path, 'utf8');
- const project = JSON.parse(json);
+ const project = JSON.parse(json);
- try {
- assertProject(project);
- } catch (error) {
- throw new Error(`${error.message} in ${path}`);
- }
+ try {
+ assertProject(project);
+ } catch (error) {
+ throw new Error(`${error.message} in ${path}`);
+ }
- return project;
+ return project;
}
const VALID_REGEXP = /^\/(.+)\/([gimsuy]*)$/;
export default function regExpFromString(pattern: string): RegExp {
- const match = pattern.match(VALID_REGEXP);
+ const match = pattern.match(VALID_REGEXP);
- if (!match) {
- throw new Error(
- `Invalid pattern ${JSON.stringify(
- pattern
- )} does not match ${VALID_REGEXP}`
- );
- }
+ if (!match) {
+ throw new Error(
+ `Invalid pattern ${JSON.stringify(
+ pattern
+ )} does not match ${VALID_REGEXP}`
+ );
+ }
- return new RegExp(match[1], match[2] || '');
+ return new RegExp(match[1], match[2] || '');
}
import {randomBytes} from 'crypto';
type Options = {
- passphrase?: string;
+ passphrase?: string;
};
type Result = {
- command: string;
- error: Error | null;
- signal: string | null;
- status: number | null;
- stderr: string;
- stdout: string;
+ command: string;
+ error: Error | null;
+ signal: string | null;
+ status: number | null;
+ stderr: string;
+ stdout: string;
};
/**
* is supplied via the `options` parameter.
*/
export default async function run(
- command: string,
- args: Array<string>,
- options: Options = {}
+ command: string,
+ args: Array<string>,
+ options: Options = {}
): Promise<Result> {
- return new Promise((resolve, reject) => {
- const prompt = `sudo[${randomBytes(16).toString('hex')}]:`;
+ return new Promise((resolve, reject) => {
+ const prompt = `sudo[${randomBytes(16).toString('hex')}]:`;
- const final = options.passphrase
- ? ['sudo', '-S', '-k', '-p', prompt, '--', command, ...args]
- : [command, ...args];
+ const final = options.passphrase
+ ? ['sudo', '-S', '-k', '-p', prompt, '--', command, ...args]
+ : [command, ...args];
- const result = {
- command: final.join(' '),
- error: null,
- signal: null,
- status: null,
- stderr: '',
- stdout: '',
- };
+ const result = {
+ command: final.join(' '),
+ error: null,
+ signal: null,
+ status: null,
+ stderr: '',
+ stdout: '',
+ };
- const child = child_process.spawn(final[0], final.slice(1));
+ const child = child_process.spawn(final[0], final.slice(1));
- // Sadly, we may see "Sorry, try again" if the wrong password is
- // supplied, because sudo may be configured to log it directly to
- // /dev/tty, not to stderr (true on macOS, not on Amazon Linux).
- //
- // See: https://github.com/sudo-project/sudo/blob/972670bf/plugins/sudoers/sudo_printf.c#L47
- child.stderr.on('data', (data) => {
- if (data.toString() === prompt) {
- child.stdin.write(`${options.passphrase}\n`);
+ // Sadly, we may see "Sorry, try again" if the wrong password is
+ // supplied, because sudo may be configured to log it directly to
+ // /dev/tty, not to stderr (true on macOS, not on Amazon Linux).
+ //
+ // See: https://github.com/sudo-project/sudo/blob/972670bf/plugins/sudoers/sudo_printf.c#L47
+ child.stderr.on('data', (data) => {
+ if (data.toString() === prompt) {
+ child.stdin.write(`${options.passphrase}\n`);
- // No point in retrying; by calling `end()` here we'll get one shot.
- child.stdin.end();
- } else {
- result.stderr += data.toString();
- }
- });
+ // No point in retrying; by calling `end()` here we'll get one shot.
+ child.stdin.end();
+ } else {
+ result.stderr += data.toString();
+ }
+ });
- child.stdout.on('data', (data) => {
- result.stdout += data.toString();
- });
+ child.stdout.on('data', (data) => {
+ result.stdout += data.toString();
+ });
- child.on('error', (error) =>
- reject({
- ...result,
- error,
- })
- );
+ child.on('error', (error) =>
+ reject({
+ ...result,
+ error,
+ })
+ );
- child.on('exit', (status, signal) => {
- if (typeof status === 'number') {
- resolve({
- ...result,
- status,
- });
- } else if (signal) {
- resolve({
- ...result,
- status,
+ child.on('exit', (status, signal) => {
+ if (typeof status === 'number') {
+ resolve({
+ ...result,
+ status,
+ });
+ } else if (signal) {
+ resolve({
+ ...result,
+ status,
+ });
+ }
});
- }
});
- });
}
import {join} from 'path';
export default function simplify(path: string) {
- const home = homedir();
+ const home = homedir();
- if (path.startsWith(home)) {
- return join('~', path.slice(home.length));
- } else {
- return path;
- }
+ if (path.startsWith(home)) {
+ return join('~', path.slice(home.length));
+ } else {
+ return path;
+ }
}
* Doesn't return stderr or stdout; resolves on success and rejects on failure.
*/
export default async function spawn(
- command: string,
- ...args: Array<string>
+ command: string,
+ ...args: Array<string>
): Promise<void> {
- return new Promise((resolve, reject) => {
- let stderr = '';
- let stdout = '';
-
- function fail(message: string) {
- const description = [command, ...args].join(' ');
- const metadata = {stderr, stdout};
-
- reject(
- new ErrorWithMetadata(`command ${description} ${message}`, metadata)
- );
- }
-
- const child = child_process.spawn(command, args, {
- stdio: ['inherit', 'pipe', 'pipe'],
- });
-
- child.stderr.on('data', (data) => (stderr += data.toString()));
- child.stdout.on('data', (data) => (stdout += data.toString()));
-
- child.on('error', (error) => {
- fail(`encountered error: ${error}`);
- });
-
- child.on('exit', (code, signal) => {
- if (code) {
- fail(`exited with status ${code}`);
- } else if (signal) {
- fail(`exited due to signal ${signal}`);
- } else {
- resolve();
- }
+ return new Promise((resolve, reject) => {
+ let stderr = '';
+ let stdout = '';
+
+ function fail(message: string) {
+ const description = [command, ...args].join(' ');
+ const metadata = {stderr, stdout};
+
+ reject(
+ new ErrorWithMetadata(
+ `command ${description} ${message}`,
+ metadata
+ )
+ );
+ }
+
+ const child = child_process.spawn(command, args, {
+ stdio: ['inherit', 'pipe', 'pipe'],
+ });
+
+ child.stderr.on('data', (data) => (stderr += data.toString()));
+ child.stdout.on('data', (data) => (stdout += data.toString()));
+
+ child.on('error', (error) => {
+ fail(`encountered error: ${error}`);
+ });
+
+ child.on('exit', (code, signal) => {
+ if (code) {
+ fail(`exited with status ${code}`);
+ } else if (signal) {
+ fail(`exited due to signal ${signal}`);
+ } else {
+ resolve();
+ }
+ });
});
- });
}
import run from './run';
type Stats = {
- group: string;
- mode: Mode;
- target?: string;
- type: 'directory' | 'file' | 'link' | 'socket' | 'special' | 'unknown';
- user: string;
+ group: string;
+ mode: Mode;
+ target?: string;
+ type: 'directory' | 'file' | 'link' | 'socket' | 'special' | 'unknown';
+ user: string;
};
const TYPE_MAP = {
- 'character device': 'special',
- 'character special file': 'special',
- directory: 'directory',
- 'regular file': 'file',
- socket: 'socket',
- 'symbolic link': 'link',
+ 'character device': 'special',
+ 'character special file': 'special',
+ directory: 'directory',
+ 'regular file': 'file',
+ socket: 'socket',
+ 'symbolic link': 'link',
} as const;
/**
* with `sudo` if necessary.
*/
export default async function stat(
- path: string
+ path: string
): Promise<Error | Stats | null> {
- if (Context.attributes.platform === 'darwin') {
- const formats = {
- group: '%Sg',
- mode: '%Lp',
- newline: '%n',
- target: '%Y',
- type: '%HT',
- user: '%Su',
- };
+ if (Context.attributes.platform === 'darwin') {
+ const formats = {
+ group: '%Sg',
+ mode: '%Lp',
+ newline: '%n',
+ target: '%Y',
+ type: '%HT',
+ user: '%Su',
+ };
- const formatString = [
- formats.mode,
- formats.type,
- formats.user,
- formats.group,
- formats.target,
- ].join(formats.newline);
+ const formatString = [
+ formats.mode,
+ formats.type,
+ formats.user,
+ formats.group,
+ formats.target,
+ ].join(formats.newline);
- // Try without sudo, then with.
- for (const sudo of [false, true]) {
- const options = sudo
- ? {passphrase: await Context.sudoPassphrase}
- : undefined;
+ // Try without sudo, then with.
+ for (const sudo of [false, true]) {
+ const options = sudo
+ ? {passphrase: await Context.sudoPassphrase}
+ : undefined;
- const {status, stderr, stdout} = await run(
- 'stat',
- ['-f', formatString, path],
- options
- );
+ const {status, stderr, stdout} = await run(
+ 'stat',
+ ['-f', formatString, path],
+ options
+ );
- if (status === 0) {
- const [mode, type, user, group, target] = stdout.split('\n');
+ if (status === 0) {
+ const [mode, type, user, group, target] = stdout.split('\n');
- const paddedMode = mode.padStart(4, '0');
+ const paddedMode = mode.padStart(4, '0');
- assertMode(paddedMode);
+ assertMode(paddedMode);
- return {
- group,
- mode: paddedMode,
- target: target || undefined,
- type: (TYPE_MAP as any)[type.toLowerCase()] || 'unknown',
- user,
- };
- }
+ return {
+ group,
+ mode: paddedMode,
+ target: target || undefined,
+ type: (TYPE_MAP as any)[type.toLowerCase()] || 'unknown',
+ user,
+ };
+ }
- if (/no such file/i.test(stderr)) {
- return null;
- } else if (!/permission denied/i.test(stderr)) {
- // Give up...
- break;
- }
+ if (/no such file/i.test(stderr)) {
+ return null;
+ } else if (!/permission denied/i.test(stderr)) {
+ // Give up...
+ break;
+ }
+ }
+ } else {
+ throw new Error('TODO: implement');
+ // GNU: command stat --printf '%a\n%F\n%G\n%U\n'
+ // a = mode
+ // F = type
+ // G = group name
+ // U = user name
+ // maybe %N (link target): prints 'src' -> 'target'
+ // 644, 1777
+ // regular file, directory, symbolic link, character special file
}
- } else {
- throw new Error('TODO: implement');
- // GNU: command stat --printf '%a\n%F\n%G\n%U\n'
- // a = mode
- // F = type
- // G = group name
- // U = user name
- // maybe %N (link target): prints 'src' -> 'target'
- // 644, 1777
- // regular file, directory, symbolic link, character special file
- }
- throw new ErrorWithMetadata(`Unable to stat ${path}`);
+ throw new ErrorWithMetadata(`Unable to stat ${path}`);
}
const MODE_REGEXP = /^0[0-7]{3}$/;
function assertMode(mode: string): asserts mode is Mode {
- if (!MODE_REGEXP.test(mode)) {
- throw new Error(`Invalid mode ${mode}`);
- }
+ if (!MODE_REGEXP.test(mode)) {
+ throw new Error(`Invalid mode ${mode}`);
+ }
}
* types (eg. a `RegExp` is printed as "/pattern/" instead of "{}" etc).
*/
export default function stringify(value: unknown) {
- let indent = '';
+ let indent = '';
- const seen = new Set<unknown>();
+ const seen = new Set<unknown>();
- function traverse(value: unknown) {
- if (
- value == null ||
- typeof value === 'boolean' ||
- typeof value === 'number' ||
- typeof value === 'symbol' ||
- value instanceof RegExp
- ) {
- return String(value);
- } else if (typeof value === 'string') {
- return JSON.stringify(value);
- } else if (Array.isArray(value)) {
- if (seen.has(value)) {
- return CIRCULAR;
- } else {
- seen.add(value);
- indent += ' ';
- let array = '[\n';
- array += value
- .map((v) => {
- return `${indent}${traverse(v)},`;
- })
- .join('\n');
- indent = indent.slice(0, -2);
- array += `\n${indent}]`;
- return array;
- }
- } else if (typeof value === 'object') {
- const toString = Object.prototype.toString.call(value);
- if (toString === '[object Object]') {
- if (seen.has(value)) {
- return CIRCULAR;
+ function traverse(value: unknown) {
+ if (
+ value == null ||
+ typeof value === 'boolean' ||
+ typeof value === 'number' ||
+ typeof value === 'symbol' ||
+ value instanceof RegExp
+ ) {
+ return String(value);
+ } else if (typeof value === 'string') {
+ return JSON.stringify(value);
+ } else if (Array.isArray(value)) {
+ if (seen.has(value)) {
+ return CIRCULAR;
+ } else {
+ seen.add(value);
+ indent += ' ';
+ let array = '[\n';
+ array += value
+ .map((v) => {
+ return `${indent}${traverse(v)},`;
+ })
+ .join('\n');
+ indent = indent.slice(0, -2);
+ array += `\n${indent}]`;
+ return array;
+ }
+ } else if (typeof value === 'object') {
+ const toString = Object.prototype.toString.call(value);
+ if (toString === '[object Object]') {
+ if (seen.has(value)) {
+ return CIRCULAR;
+ } else {
+ seen.add(value);
+ indent += ' ';
+ let object = '{\n';
+ object += Object.entries(value!)
+ .map(([key, value]) => {
+ return `${indent}${JSON.stringify(key)}: ${traverse(
+ value
+ )},`;
+ })
+ .join('\n');
+ indent = indent.slice(0, -2);
+ object += `\n${indent}}`;
+ return object;
+ }
+ } else {
+ return toString;
+ }
+ } else if (typeof value === 'function') {
+ return value
+ .toString()
+ .split('\n')
+ .map((line, i) => {
+ return i ? `${indent}${line}` : line;
+ })
+ .join('\n');
} else {
- seen.add(value);
- indent += ' ';
- let object = '{\n';
- object += Object.entries(value!)
- .map(([key, value]) => {
- return `${indent}${JSON.stringify(key)}: ${traverse(value)},`;
- })
- .join('\n');
- indent = indent.slice(0, -2);
- object += `\n${indent}}`;
- return object;
+ return `${LAQUO}unknown${RAQUO}`;
}
- } else {
- return toString;
- }
- } else if (typeof value === 'function') {
- return value
- .toString()
- .split('\n')
- .map((line, i) => {
- return i ? `${indent}${line}` : line;
- })
- .join('\n');
- } else {
- return `${LAQUO}unknown${RAQUO}`;
}
- }
- return traverse(value);
+ return traverse(value);
}
* leave them on the disk for debugging purposes.
*/
export default async function tempfile(contents: string): Promise<string> {
- const name = randomBytes(16).toString('hex');
+ const name = randomBytes(16).toString('hex');
- const path = join(TMP_DIR, name);
+ const path = join(TMP_DIR, name);
- await fs.writeFile(path, contents, 'utf8');
+ await fs.writeFile(path, contents, 'utf8');
- log.debug(`Wrote ${contents.length} bytes to ${path}`);
+ log.debug(`Wrote ${contents.length} bytes to ${path}`);
- return path;
+ return path;
}
export type Scope = {
- [property: string]: JSONValue;
+ [property: string]: JSONValue;
};
/**
* be evaluated to produce the template output).
*/
export function compile(source: string) {
- let output = 'let __buffer__ = "";\n';
-
- let context = 'TemplateText';
-
- for (const token of tokenize(source)) {
- if (token.kind === 'TemplateText') {
- output += `__buffer__ += ${JSON.stringify(token.text)};\n`;
- } else if (token.kind === 'HostText') {
- if (context === 'Expression') {
- output += `__buffer__ += (${token.text.trim()});\n`;
- } else if (context === 'Statement') {
- output += `${token.text.trim()}\n`;
- }
- } else if (token.kind === 'StartExpression') {
- context = 'Expression';
- } else if (token.kind === 'StartStatement') {
- context = 'Statement';
- } else if (token.kind === 'EndDelimiter') {
- context = 'TemplateText';
+ let output = 'let __buffer__ = "";\n';
+
+ let context = 'TemplateText';
+
+ for (const token of tokenize(source)) {
+ if (token.kind === 'TemplateText') {
+ output += `__buffer__ += ${JSON.stringify(token.text)};\n`;
+ } else if (token.kind === 'HostText') {
+ if (context === 'Expression') {
+ output += `__buffer__ += (${token.text.trim()});\n`;
+ } else if (context === 'Statement') {
+ output += `${token.text.trim()}\n`;
+ }
+ } else if (token.kind === 'StartExpression') {
+ context = 'Expression';
+ } else if (token.kind === 'StartStatement') {
+ context = 'Statement';
+ } else if (token.kind === 'EndDelimiter') {
+ context = 'TemplateText';
+ }
}
- }
- output += 'return __buffer__;\n';
+ output += 'return __buffer__;\n';
- return output;
+ return output;
}
/**
* producing the final string result.
*/
export function fill(compiled: string, scope: Scope = {}) {
- const context = Object.entries(scope).map(
- ([key, value]) => `const ${key} = ${JSON.stringify(value)};\n`
- );
+ const context = Object.entries(scope).map(
+ ([key, value]) => `const ${key} = ${JSON.stringify(value)};\n`
+ );
- const sandbox = new Function(context + compiled);
+ const sandbox = new Function(context + compiled);
- return sandbox();
+ return sandbox();
}
type Token =
- | EndDelimiter
- | HostText
- | StartExpression
- | StartStatement
- | TemplateText;
+ | EndDelimiter
+ | HostText
+ | StartExpression
+ | StartStatement
+ | TemplateText;
type EndDelimiter = {
- kind: 'EndDelimiter';
+ kind: 'EndDelimiter';
};
type HostText = {
- kind: 'HostText';
- text: string;
+ kind: 'HostText';
+ text: string;
};
type StartExpression = {
- kind: 'StartExpression';
+ kind: 'StartExpression';
};
type StartStatement = {
- kind: 'StartStatement';
+ kind: 'StartStatement';
};
type TemplateText = {
- kind: 'TemplateText';
- text: string;
+ kind: 'TemplateText';
+ text: string;
};
/**
*
*/
export function* tokenize(input: string): Generator<Token> {
- const delimiter = /(<%=|<%-|<%|-%>|%>)/g;
-
- let i = 0;
-
- let inHost = false;
-
- while (i < input.length) {
- const match = delimiter.exec(input);
-
- if (match) {
- const text = match[0];
-
- if (inHost) {
- if (text.endsWith('%>')) {
- yield {
- kind: 'HostText',
- text: input.slice(i, match.index),
- };
-
- yield {
- kind: 'EndDelimiter',
- };
-
- inHost = false;
-
- if (text === '-%>') {
- // Remove next character if it is a newline.
- if (input[delimiter.lastIndex] === '\n') {
- delimiter.lastIndex++;
- i = match.index! + text.length + 1;
- continue;
+ const delimiter = /(<%=|<%-|<%|-%>|%>)/g;
+
+ let i = 0;
+
+ let inHost = false;
+
+ while (i < input.length) {
+ const match = delimiter.exec(input);
+
+ if (match) {
+ const text = match[0];
+
+ if (inHost) {
+ if (text.endsWith('%>')) {
+ yield {
+ kind: 'HostText',
+ text: input.slice(i, match.index),
+ };
+
+ yield {
+ kind: 'EndDelimiter',
+ };
+
+ inHost = false;
+
+ if (text === '-%>') {
+ // Remove next character if it is a newline.
+ if (input[delimiter.lastIndex] === '\n') {
+ delimiter.lastIndex++;
+ i = match.index! + text.length + 1;
+ continue;
+ }
+ }
+ } else {
+ throw new Error(
+ `Unexpected start delimiter "${text}" at index ${match.index}`
+ );
+ }
+ } else {
+ if (text === '<%-') {
+ // Eat whitspace between previous newline and delimiter.
+ yield {
+ kind: 'TemplateText',
+ text: input
+ .slice(i, match.index)
+ .replace(/(^|\n)[ \t]+$/, '$1'),
+ };
+ } else {
+ yield {
+ kind: 'TemplateText',
+ text: input.slice(i, match.index),
+ };
+ }
+
+ inHost = true;
+
+ if (text === '<%=') {
+ yield {
+ kind: 'StartExpression',
+ };
+ } else if (text.startsWith('<%')) {
+ yield {
+ kind: 'StartStatement',
+ };
+ } else if (text === '%>') {
+ throw new Error(
+ `Unexpected end delimiter "%>" at index ${match.index}`
+ );
+ }
}
- }
- } else {
- throw new Error(
- `Unexpected start delimiter "${text}" at index ${match.index}`
- );
- }
- } else {
- if (text === '<%-') {
- // Eat whitspace between previous newline and delimiter.
- yield {
- kind: 'TemplateText',
- text: input.slice(i, match.index).replace(/(^|\n)[ \t]+$/, '$1'),
- };
+
+ i = match.index! + text.length;
} else {
- yield {
- kind: 'TemplateText',
- text: input.slice(i, match.index),
- };
- }
+ yield {
+ kind: 'TemplateText',
+ text: input.slice(i),
+ };
- inHost = true;
-
- if (text === '<%=') {
- yield {
- kind: 'StartExpression',
- };
- } else if (text.startsWith('<%')) {
- yield {
- kind: 'StartStatement',
- };
- } else if (text === '%>') {
- throw new Error(
- `Unexpected end delimiter "%>" at index ${match.index}`
- );
+ break;
}
- }
-
- i = match.index! + text.length;
- } else {
- yield {
- kind: 'TemplateText',
- text: input.slice(i),
- };
-
- break;
}
- }
}
let context: Array<string> = [];
export function describe(description: string, callback: () => void) {
- context.push(description);
- callback();
- context.pop();
+ context.push(description);
+ callback();
+ context.pop();
}
export function expect(value: unknown) {
- return {
- toBe(expected: unknown) {
- assert.strictEqual(
- value,
- expected,
- `Expected ${stringify(value)} to be ${stringify(expected)}`
- );
- },
-
- toEqual(expected: unknown) {
- assert.deepStrictEqual(
- value,
- expected,
- `Expected ${stringify(value)} to equal ${stringify(expected)}`
- );
- },
-
- toMatch(expected: unknown) {
- if (expected instanceof RegExp) {
- assert(
- expected.test(String(value)),
- `Expected ${stringify(value)} to match ${stringify(expected)}`
- );
- } else {
- throw new Error(`Expected RegExp but received ${typeof expected}`);
- }
- },
-
- toThrow(expected: string | typeof Error | RegExp) {
- let caught;
-
- try {
- if (typeof value === 'function') {
- value();
- } else {
- throw new Error(`Expected function but received ${typeof value}`);
- }
- } catch (error) {
- caught = error;
- }
-
- if (!caught) {
- assert.fail('Expected error but none was thrown');
- } else {
- const message = caught.toString();
-
- if (typeof expected === 'string') {
- assert.ok(
- message.includes(expected),
- `Expected message ${stringify(message)} to contain ${stringify(
- expected
- )}`
- );
- } else if (expected instanceof RegExp) {
- assert.ok(
- expected.test(message),
- `Expected message ${stringify(message)} to match ${stringify(
- expected
- )}`
- );
- } else {
- assert.ok(
- caught instanceof expected,
- `Expected error to be instance of ${expected}`
- );
- }
- }
- },
- };
+ return {
+ toBe(expected: unknown) {
+ assert.strictEqual(
+ value,
+ expected,
+ `Expected ${stringify(value)} to be ${stringify(expected)}`
+ );
+ },
+
+ toEqual(expected: unknown) {
+ assert.deepStrictEqual(
+ value,
+ expected,
+ `Expected ${stringify(value)} to equal ${stringify(expected)}`
+ );
+ },
+
+ toMatch(expected: unknown) {
+ if (expected instanceof RegExp) {
+ assert(
+ expected.test(String(value)),
+ `Expected ${stringify(value)} to match ${stringify(
+ expected
+ )}`
+ );
+ } else {
+ throw new Error(
+ `Expected RegExp but received ${typeof expected}`
+ );
+ }
+ },
+
+ toThrow(expected: string | typeof Error | RegExp) {
+ let caught;
+
+ try {
+ if (typeof value === 'function') {
+ value();
+ } else {
+ throw new Error(
+ `Expected function but received ${typeof value}`
+ );
+ }
+ } catch (error) {
+ caught = error;
+ }
+
+ if (!caught) {
+ assert.fail('Expected error but none was thrown');
+ } else {
+ const message = caught.toString();
+
+ if (typeof expected === 'string') {
+ assert.ok(
+ message.includes(expected),
+ `Expected message ${stringify(
+ message
+ )} to contain ${stringify(expected)}`
+ );
+ } else if (expected instanceof RegExp) {
+ assert.ok(
+ expected.test(message),
+ `Expected message ${stringify(
+ message
+ )} to match ${stringify(expected)}`
+ );
+ } else {
+ assert.ok(
+ caught instanceof expected,
+ `Expected error to be instance of ${expected}`
+ );
+ }
+ }
+ },
+ };
}
export function test(description: string, callback: () => void) {
- TESTS.push([[...context, description].join(` ${RAQUO} `), callback]);
+ TESTS.push([[...context, description].join(` ${RAQUO} `), callback]);
}
export async function run() {
- const start = Date.now();
-
- let failureCount = 0;
- let successCount = 0;
-
- debug(() => log());
-
- for (const [description, callback] of TESTS) {
- try {
- // Need to stay within one line if `clear()` calls below are to work.
- const trimmedDescription = description.slice(
- 0,
- process.stderr.columns - ' TEST '.length - 1
- );
-
- debug(() => print(yellow.reverse` TEST `, trimmedDescription));
- await callback();
- successCount++;
- await debug(async () => {
- await print.clear();
- log(green.reverse` PASS `, description);
- });
- } catch (error) {
- failureCount++;
- await print.clear();
- log(red.reverse` FAIL `, description);
- log(`\n${error.message}\n`);
- log(error);
- log();
+ const start = Date.now();
+
+ let failureCount = 0;
+ let successCount = 0;
+
+ debug(() => log());
+
+ for (const [description, callback] of TESTS) {
+ try {
+ // Need to stay within one line if `clear()` calls below are to work.
+ const trimmedDescription = description.slice(
+ 0,
+ process.stderr.columns - ' TEST '.length - 1
+ );
+
+ debug(() => print(yellow.reverse` TEST `, trimmedDescription));
+ await callback();
+ successCount++;
+ await debug(async () => {
+ await print.clear();
+ log(green.reverse` PASS `, description);
+ });
+ } catch (error) {
+ failureCount++;
+ await print.clear();
+ log(red.reverse` FAIL `, description);
+ log(`\n${error.message}\n`);
+ log(error);
+ log();
+ }
}
- }
- const elapsed = ((Date.now() - start) / 1000).toFixed(2);
+ const elapsed = ((Date.now() - start) / 1000).toFixed(2);
- const successSummary = successCount
- ? green.bold`${successCount} passed`
- : '0 passed';
+ const successSummary = successCount
+ ? green.bold`${successCount} passed`
+ : '0 passed';
- const failureSummary = failureCount
- ? red.bold`${failureCount} failed`
- : `0 failed`;
+ const failureSummary = failureCount
+ ? red.bold`${failureCount} failed`
+ : `0 failed`;
- const totalSummary = `${successCount + failureCount} total in ${elapsed}s`;
+ const totalSummary = `${successCount + failureCount} total in ${elapsed}s`;
- const logLevel = getLogLevel();
+ const logLevel = getLogLevel();
- if (logLevel >= LOG_LEVEL.DEBUG || failureCount) {
- log();
- log(`${successSummary}, ${failureSummary}, ${totalSummary}`);
- if (logLevel < LOG_LEVEL.DEBUG) {
- log('Rerun with --debug to see full results');
+ if (logLevel >= LOG_LEVEL.DEBUG || failureCount) {
+ log();
+ log(`${successSummary}, ${failureSummary}, ${totalSummary}`);
+ if (logLevel < LOG_LEVEL.DEBUG) {
+ log('Rerun with --debug to see full results');
+ }
+ log();
}
- log();
- }
- if (failureCount) {
- throw new ErrorWithMetadata('Test suite failed');
- }
+ if (failureCount) {
+ throw new ErrorWithMetadata('Test suite failed');
+ }