Commit cff58e9d authored by Joe Wilm's avatar Joe Wilm Committed by GitHub

Merge pull request #1147 from jwilm/scrollback

Scrollback
parents 865727c0 054e38e9
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## Version 0.2.0
### Added
- Add a scrollback history buffer (10_000 lines by default)
- CHANGELOG has been added for documenting relevant user-facing changes
- Add `ClearHistory` key binding action and the `Erase Saved Lines` control sequence
- When growing the window height, Alacritty will now try to load additional lines out of the
scrollback history
- Support the dim foreground color (`echo -e '\033[2mDimmed Text'`)
- Add support for the LCD-V pixel mode (vertical screens)
- Pressing enter on the numpad should now insert a newline
- The mouse bindings now support keyboard modifiers (shift/ctrl/alt/super)
- Add support for the bright foreground color
### Changed
- Multiple key/mouse bindings for a single key will now all be executed instead of picking one and
ignoring the rest
- Improve text scrolling performance (affects applications like `yes`, not scrolling the history)
### Fixed
- Clear the visible region when the RIS escape sequence (`echo -ne '\033c'`) is received
- Prevent logger from crashing Alacritty when stdout/stderr is not available
- Fix a crash when sending the IL escape sequence with a large number of lines
......@@ -8,7 +8,7 @@ dependencies = [
[[package]]
name = "alacritty"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"arraydeque 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)",
......
[package]
name = "alacritty"
version = "0.1.0"
version = "0.2.0"
authors = ["Joe Wilm <joe@jwilm.com>"]
license = "Apache-2.0"
build = "build.rs"
......
......@@ -385,12 +385,6 @@ Just Works.
It sounds like you deleted some key bindings from your config file. Please
reference the default config file to restore them.
- **_Why doesn't it support scrollback?_**
Alacritty's original purpose was to provide a better experience when using
[tmux] which already handled scrollback. The scope of this project has since
expanded, and [scrollback will eventually be added](https://github.com/jwilm/alacritty/issues/124).
## IRC
Alacritty discussion can be found in `#alacritty` on freenode.
......
.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.5.
.TH ALACRITTY "1" "March 2018" "alacritty 0.1.0" "User Commands"
.TH ALACRITTY "1" "August 2018" "alacritty 0.2.0" "User Commands"
.SH NAME
alacritty \- a cross-platform, gpu-accelerated terminal emulator
.SH "SYNOPSIS"
......
......@@ -33,6 +33,29 @@ window:
# Setting this to false will result in window without borders and title bar.
decorations: true
scrolling:
# How many lines of scrollback to keep,
# '0' will disable scrolling.
history: 10000
# Number of lines the viewport will move for every line
# scrolled when scrollback is enabled (history > 0).
multiplier: 3
# Faux Scrolling
#
# The `faux_multiplier` setting controls the number
# of lines the terminal should scroll when the alternate
# screen buffer is active. This is used to allow mouse
# scrolling for applications like `man`.
#
# To disable this completely, set `faux_multiplier` to 0.
faux_multiplier: 3
# Automatically scroll to the bottom when new text is written
# to the terminal.
auto_scroll: false
# Display tabs using this many cells (changes require restart)
tabspaces: 8
......@@ -221,16 +244,6 @@ mouse:
double_click: { threshold: 300 }
triple_click: { threshold: 300 }
# Faux Scrollback
#
# The `faux_scrollback_lines` setting controls the number
# of lines the terminal should scroll when the alternate
# screen buffer is active. This is used to allow mouse
# scrolling for applications like `man`.
#
# To disable this completely, set `faux_scrollback_lines` to 0.
faux_scrollback_lines: 1
selection:
semantic_escape_chars: ",│`|:\"' ()[]{}<>"
......@@ -290,7 +303,18 @@ live_config_reload: true
# around them.
#
# Either an `action`, `chars`, or `command` field must be present.
# `action` must be one of `Paste`, `PasteSelection`, `Copy`, or `Quit`.
# `action` must be one of the following:
# - `Paste`
# - `PasteSelection`
# - `Copy`
# - `IncreaseFontSize`
# - `DecreaseFontSize`
# - `ResetFontSize`
# - `ScrollPageUp`
# - `ScrollPageDown`
# - `ScrollToTop`
# - `ScrollToBottom`
# - `Quit`
# `chars` writes the specified string every time that binding is activated.
# These should generally be escape sequences, but they can be configured to
# send arbitrary strings of bytes.
......
......@@ -31,6 +31,29 @@ window:
# Setting this to false will result in window without borders and title bar.
decorations: true
scrolling:
# How many lines of scrollback to keep,
# '0' will disable scrolling.
history: 10000
# Number of lines the viewport will move for every line
# scrolled when scrollback is enabled (history > 0).
multiplier: 3
# Faux Scrolling
#
# The `faux_multiplier` setting controls the number
# of lines the terminal should scroll when the alternate
# screen buffer is active. This is used to allow mouse
# scrolling for applications like `man`.
#
# To disable this completely, set `faux_multiplier` to 0.
faux_multiplier: 3
# Automatically scroll to the bottom when new text is written
# to the terminal.
auto_scroll: false
# Display tabs using this many cells (changes require restart)
tabspaces: 8
......@@ -200,16 +223,6 @@ mouse:
double_click: { threshold: 300 }
triple_click: { threshold: 300 }
# Faux Scrollback
#
# The `faux_scrollback_lines` setting controls the number
# of lines the terminal should scroll when the alternate
# screen buffer is active. This is used to allow mouse
# scrolling for applications like `man`.
#
# To disable this completely, set `faux_scrollback_lines` to 0.
faux_scrollback_lines: 1
selection:
semantic_escape_chars: ",│`|:\"' ()[]{}<>"
......@@ -269,7 +282,18 @@ live_config_reload: true
# around them.
#
# Either an `action`, `chars`, or `command` field must be present.
# `action` must be one of `Paste`, `PasteSelection`, `Copy`, or `Quit`.
# `action` must be one of the following:
# - `Paste`
# - `PasteSelection`
# - `Copy`
# - `IncreaseFontSize`
# - `DecreaseFontSize`
# - `ResetFontSize`
# - `ScrollPageUp`
# - `ScrollPageDown`
# - `ScrollToTop`
# - `ScrollToBottom`
# - `Quit`
# `chars` writes the specified string every time that binding is activated.
# These should generally be escape sequences, but they can be configured to
# send arbitrary strings of bytes.
......@@ -291,6 +315,8 @@ key_bindings:
- { key: Key0, mods: Command, action: ResetFontSize }
- { key: Equals, mods: Command, action: IncreaseFontSize }
- { key: Minus, mods: Command, action: DecreaseFontSize }
- { key: K, mods: Command, action: ClearHistory }
- { key: K, mods: Command, chars: "\x0c" }
- { key: PageUp, mods: Shift, chars: "\x1b[5;2~" }
- { key: PageUp, mods: Control, chars: "\x1b[5;5~" }
- { key: PageUp, chars: "\x1b[5~" }
......
......@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<string>0.2.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
......
#!/usr/bin/env ruby
require 'json'
Dir.glob('./tests/ref/**/grid.json').each do |path|
puts "Migrating #{path}"
# Read contents
s = File.open(path) { |f| f.read }
# Parse
grid = JSON.parse(s)
# Normalize Storage serialization
if grid['raw'].is_a? Array
grid['raw'] = {
'inner' => grid['raw'][0],
'zero' => grid['raw'][1],
'visible_lines' => grid['raw'][2]
}
end
# Migrate Row serialization
grid['raw']['inner'].map! do |row|
if row.is_a? Hash
row
else
{ inner: row, occ: row.length }
end
end
# Write updated grid
File.open(path, 'w') { |f| f << JSON.generate(grid) }
end
name: alacritty # you probably want to 'snapcraft register <name>'
version: '0.1.0' # just for humans, typically '1.2+git' or '1.3.2'
version: '0.2.0' # just for humans, typically '1.2+git' or '1.3.2'
summary: Modern, GPU accelerated terminal emulator # 79 char long summary
description: |
Modern, GPU accelerated terminal emulator
......@@ -19,4 +19,4 @@ parts:
apps:
alacritty:
command: env XDG_RUNTIME_DIR= XDG_CONFIG_HOME=$SNAP_USER_DATA XDG_DATA_DIRS=$SNAP_DATA PATH=$SNAP/bin:$PATH SNAP= alacritty
desktop: Alacritty.desktop
\ No newline at end of file
desktop: Alacritty.desktop
......@@ -83,26 +83,9 @@ pub struct Mouse {
#[serde(default, deserialize_with = "failure_default")]
pub triple_click: ClickHandler,
/// up/down arrows sent when scrolling in alt screen buffer
#[serde(deserialize_with = "deserialize_faux_scrollback_lines")]
#[serde(default="default_faux_scrollback_lines")]
pub faux_scrollback_lines: usize,
}
fn default_faux_scrollback_lines() -> usize {
1
}
fn deserialize_faux_scrollback_lines<'a, D>(deserializer: D) -> ::std::result::Result<usize, D::Error>
where D: de::Deserializer<'a>
{
match usize::deserialize(deserializer) {
Ok(lines) => Ok(lines),
Err(err) => {
eprintln!("problem with config: {}; Using default value", err);
Ok(default_faux_scrollback_lines())
},
}
// TODO: DEPRECATED
#[serde(default)]
pub faux_scrollback_lines: Option<usize>,
}
impl Default for Mouse {
......@@ -114,7 +97,7 @@ impl Default for Mouse {
triple_click: ClickHandler {
threshold: Duration::from_millis(300),
},
faux_scrollback_lines: 1,
faux_scrollback_lines: None,
}
}
}
......@@ -401,6 +384,10 @@ pub struct Config {
/// Number of spaces in one tab
#[serde(default="default_tabspaces", deserialize_with = "deserialize_tabspaces")]
tabspaces: usize,
/// How much scrolling history to keep
#[serde(default, deserialize_with="failure_default")]
scrolling: Scrolling,
}
fn failure_default_vec<'a, D, T>(deserializer: D) -> ::std::result::Result<Vec<T>, D::Error>
......@@ -484,6 +471,66 @@ impl Default for Config {
}
}
/// Struct for scrolling related settings
#[derive(Copy, Clone, Debug, Deserialize)]
pub struct Scrolling {
#[serde(deserialize_with="deserialize_scrolling_history")]
#[serde(default="default_scrolling_history")]
pub history: u32,
#[serde(deserialize_with="deserialize_scrolling_multiplier")]
#[serde(default="default_scrolling_multiplier")]
pub multiplier: u8,
#[serde(deserialize_with="deserialize_scrolling_multiplier")]
#[serde(default="default_scrolling_multiplier")]
pub faux_multiplier: u8,
#[serde(default, deserialize_with="failure_default")]
pub auto_scroll: bool,
}
fn default_scrolling_history() -> u32 {
10_000
}
// Default for normal and faux scrolling
fn default_scrolling_multiplier() -> u8 {
3
}
impl Default for Scrolling {
fn default() -> Self {
Self {
history: default_scrolling_history(),
multiplier: default_scrolling_multiplier(),
faux_multiplier: default_scrolling_multiplier(),
auto_scroll: false,
}
}
}
fn deserialize_scrolling_history<'a, D>(deserializer: D) -> ::std::result::Result<u32, D::Error>
where D: de::Deserializer<'a>
{
match u32::deserialize(deserializer) {
Ok(lines) => Ok(lines),
Err(err) => {
eprintln!("problem with config: {}; Using default value", err);
Ok(default_scrolling_history())
},
}
}
fn deserialize_scrolling_multiplier<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error>
where D: de::Deserializer<'a>
{
match u8::deserialize(deserializer) {
Ok(lines) => Ok(lines),
Err(err) => {
eprintln!("problem with config: {}; Using default value", err);
Ok(default_scrolling_multiplier())
},
}
}
/// Newtype for implementing deserialize on glutin Mods
///
/// Our deserialize impl wouldn't be covered by a derive(Deserialize); see the
......@@ -549,7 +596,9 @@ impl<'a> de::Deserialize<'a> for ActionWrapper {
type Value = ActionWrapper;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Paste, Copy, PasteSelection, IncreaseFontSize, DecreaseFontSize, ResetFontSize, Hide, or Quit")
f.write_str("Paste, Copy, PasteSelection, IncreaseFontSize, DecreaseFontSize, \
ResetFontSize, ScrollPageUp, ScrollPageDown, ScrollToTop, \
ScrollToBottom, ClearHistory, Hide, or Quit")
}
fn visit_str<E>(self, value: &str) -> ::std::result::Result<ActionWrapper, E>
......@@ -562,6 +611,11 @@ impl<'a> de::Deserialize<'a> for ActionWrapper {
"IncreaseFontSize" => Action::IncreaseFontSize,
"DecreaseFontSize" => Action::DecreaseFontSize,
"ResetFontSize" => Action::ResetFontSize,
"ScrollPageUp" => Action::ScrollPageUp,
"ScrollPageDown" => Action::ScrollPageDown,
"ScrollToTop" => Action::ScrollToTop,
"ScrollToBottom" => Action::ScrollToBottom,
"ClearHistory" => Action::ClearHistory,
"Hide" => Action::Hide,
"Quit" => Action::Quit,
_ => return Err(E::invalid_value(Unexpected::Str(value), &self)),
......@@ -1407,6 +1461,17 @@ impl Config {
self.dynamic_title
}
/// Scrolling settings
#[inline]
pub fn scrolling(&self) -> Scrolling {
self.scrolling
}
// Update the history size, used in ref tests
pub fn set_history(&mut self, history: u32) {
self.scrolling.history = history;
}
pub fn load_from<P: Into<PathBuf>>(path: P) -> Result<Config> {
let path = path.into();
let raw = Config::read_file(path.as_path())?;
......@@ -1447,6 +1512,11 @@ impl Config {
eprintln!("{}", fmt::Yellow("Config `padding` is deprecated. \
Please use `window.padding` instead."));
}
if self.mouse.faux_scrollback_lines.is_some() {
println!("{}", fmt::Yellow("Config `mouse.faux_scrollback_lines` is deprecated. \
Please use `mouse.faux_scrolling_lines` instead."));
}
}
}
......
......@@ -24,7 +24,6 @@ use config::Config;
use font::{self, Rasterize};
use meter::Meter;
use renderer::{self, GlyphCache, QuadRenderer};
use selection::Selection;
use term::{Term, SizeInfo};
use window::{self, Size, Pixels, Window, SetInnerSize};
......@@ -326,7 +325,7 @@ impl Display {
/// A reference to Term whose state is being drawn must be provided.
///
/// This call may block if vsync is enabled
pub fn draw(&mut self, mut terminal: MutexGuard<Term>, config: &Config, selection: Option<&Selection>) {
pub fn draw(&mut self, mut terminal: MutexGuard<Term>, config: &Config) {
// Clear dirty flag
terminal.dirty = !terminal.visual_bell.completed();
......@@ -374,7 +373,7 @@ impl Display {
// Draw the grid
api.render_cells(
terminal.renderable_cells(config, selection, window_focused),
terminal.renderable_cells(config, window_focused),
glyph_cache,
);
});
......
......@@ -10,6 +10,8 @@ use parking_lot::MutexGuard;
use glutin::{self, ModifiersState, Event, ElementState};
use copypasta::{Clipboard, Load, Store};
use ansi::{Handler, ClearMode};
use grid::Scroll;
use config::{self, Config};
use cli::Options;
use display::OnResize;
......@@ -33,10 +35,8 @@ pub trait Notify {
pub struct ActionContext<'a, N: 'a> {
pub notifier: &'a mut N,
pub terminal: &'a mut Term,
pub selection: &'a mut Option<Selection>,
pub size_info: &'a SizeInfo,
pub mouse: &'a mut Mouse,
pub selection_modified: bool,
pub received_count: &'a mut usize,
pub suppress_chars: &'a mut bool,
pub last_modifiers: &'a mut ModifiersState,
......@@ -56,51 +56,61 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> {
*self.size_info
}
fn scroll(&mut self, scroll: Scroll) {
self.terminal.scroll_display(scroll);
}
fn clear_history(&mut self) {
self.terminal.clear_screen(ClearMode::Saved);
}
fn copy_selection(&self, buffer: ::copypasta::Buffer) {
if let Some(ref selection) = *self.selection {
if let Some(ref span) = selection.to_span(self.terminal) {
let buf = self.terminal.string_from_selection(&span);
if !buf.is_empty() {
Clipboard::new()
.and_then(|mut clipboard| clipboard.store(buf, buffer))
.unwrap_or_else(|err| {
warn!("Error storing selection to clipboard. {}", Red(err));
});
}
if let Some(selected) = self.terminal.selection_to_string() {
if !selected.is_empty() {
Clipboard::new()
.and_then(|mut clipboard| clipboard.store(selected, buffer))
.unwrap_or_else(|err| {
warn!("Error storing selection to clipboard. {}", Red(err));
});
}
}
}
fn clear_selection(&mut self) {
*self.selection = None;
self.selection_modified = true;
*self.terminal.selection_mut() = None;
self.terminal.dirty = true;
}
fn update_selection(&mut self, point: Point, side: Side) {
self.selection_modified = true;
self.terminal.dirty = true;
let point = self.terminal.visible_to_buffer(point);
// Update selection if one exists
if let Some(ref mut selection) = *self.selection {
if let Some(ref mut selection) = *self.terminal.selection_mut() {
selection.update(point, side);
return;
}
// Otherwise, start a regular selection
self.simple_selection(point, side);
*self.terminal.selection_mut() = Some(Selection::simple(point, side));
}
fn simple_selection(&mut self, point: Point, side: Side) {
*self.selection = Some(Selection::simple(point, side));
self.selection_modified = true;
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::simple(point, side));
self.terminal.dirty = true;
}
fn semantic_selection(&mut self, point: Point) {
*self.selection = Some(Selection::semantic(point, self.terminal));
self.selection_modified = true;
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::semantic(point));
self.terminal.dirty = true;
}
fn line_selection(&mut self, point: Point) {
*self.selection = Some(Selection::lines(point));
self.selection_modified = true;
let point = self.terminal.visible_to_buffer(point);
*self.terminal.selection_mut() = Some(Selection::lines(point));
self.terminal.dirty = true;
}
fn mouse_coords(&self) -> Option<Point> {
......@@ -172,8 +182,8 @@ pub enum ClickState {
/// State of the mouse
pub struct Mouse {
pub x: u32,
pub y: u32,
pub x: usize,
pub y: usize,
pub left_button_state: ElementState,
pub middle_button_state: ElementState,
pub right_button_state: ElementState,
......@@ -213,6 +223,7 @@ pub struct Processor<N> {
key_bindings: Vec<KeyBinding>,
mouse_bindings: Vec<MouseBinding>,
mouse_config: config::Mouse,
scrolling_config: config::Scrolling,
print_events: bool,
wait_for_event: bool,
notifier: N,
......@@ -220,7 +231,6 @@ pub struct Processor<N> {
resize_tx: mpsc::Sender<(u32, u32)>,
ref_test: bool,
size_info: SizeInfo,
pub selection: Option<Selection>,
hide_cursor_when_typing: bool,
hide_cursor: bool,
received_count: usize,
......@@ -236,7 +246,6 @@ pub struct Processor<N> {
impl<N> OnResize for Processor<N> {
fn on_resize(&mut self, size: &SizeInfo) {
self.size_info = size.to_owned();
self.selection = None;
}
}
......@@ -257,13 +266,13 @@ impl<N: Notify> Processor<N> {
key_bindings: config.key_bindings().to_vec(),
mouse_bindings: config.mouse_bindings().to_vec(),
mouse_config: config.mouse().to_owned(),
scrolling_config: config.scrolling(),
print_events: options.print_events,
wait_for_event: true,
notifier,
resize_tx,
ref_test,
mouse: Default::default(),
selection: None,
size_info,
hide_cursor_when_typing: config.hide_cursor_when_typing(),
hide_cursor: false,
......@@ -295,7 +304,8 @@ impl<N: Notify> Processor<N> {
CloseRequested => {
if ref_test {
// dump grid state
let grid = processor.ctx.terminal.grid();
let mut grid = processor.ctx.terminal.grid().clone();
grid.truncate();
let serialized_grid = json::to_string(&grid)
.expect("serialize grid");
......@@ -338,17 +348,11 @@ impl<N: Notify> Processor<N> {
}
},
CursorMoved { position: (x, y), modifiers, .. } => {
let x = x as i32;
let y = y as i32;
let x = limit(x, 0, processor.ctx.size_info.width as i32);
let y = limit(y, 0, processor.ctx.size_info.height as i32);
let x = limit(x as i32, 0, processor.ctx.size_info.width as i32);
let y = limit(y as i32, 0, processor.ctx.size_info.height as i32);
*hide_cursor = false;
processor.mouse_moved(x as u32, y as u32, modifiers);
if !processor.ctx.selection.is_none() {
processor.ctx.terminal.dirty = true;
}
processor.mouse_moved(x as usize, y as usize, modifiers);
},
MouseWheel { delta, phase, modifiers, .. } => {
*hide_cursor = false;
......@@ -424,10 +428,8 @@ impl<N: Notify> Processor<N> {
context = ActionContext {
terminal: &mut terminal,
notifier: &mut self.notifier,
selection: &mut self.selection,
mouse: &mut self.mouse,
size_info: &self.size_info,
selection_modified: false,
received_count: &mut self.received_count,
suppress_chars: &mut self.suppress_chars,
last_modifiers: &mut self.last_modifiers,
......@@ -436,6 +438,7 @@ impl<N: Notify> Processor<N> {
processor = input::Processor {
ctx: context,
scrolling_config: &self.scrolling_config,
mouse_config: &self.mouse_config,
key_bindings: &self.key_bindings[..],
mouse_bindings: &self.mouse_bindings[..],
......@@ -473,10 +476,10 @@ impl<N: Notify> Processor<N> {
}
window.is_focused = window_is_focused;
}
if processor.ctx.selection_modified {
processor.ctx.terminal.dirty = true;
}
if self.window_changes.hide {
window.hide();
}
if self.window_changes.hide {
......
......@@ -255,6 +255,9 @@ impl<Io> EventLoop<Io>
let mut processed = 0;
let mut terminal = None;
// Flag to keep track if wakeup has already been sent
let mut send_wakeup = false;
loop {
match self.pty.read(&mut buf[..]) {
Ok(0) => break,
......@@ -272,10 +275,14 @@ impl<Io> EventLoop<Io>
// Get reference to terminal. Lock is acquired on initial
// iteration and held until there's no bytes left to parse
// or we've reached MAX_READ.
if terminal.is_none() {
let terminal = if terminal.is_none() {
terminal = Some(self.terminal.lock());
}
let terminal = terminal.as_mut().unwrap();
let terminal = terminal.as_mut().unwrap();
send_wakeup = !terminal.dirty;
terminal
} else {
terminal.as_mut().unwrap()
};
// Run the parser
for byte in &buf[..got] {
......@@ -301,7 +308,7 @@ impl<Io> EventLoop<Io>
// Only request a draw if one hasn't already been requested.
if let Some(mut terminal) = terminal {
if !terminal.dirty {
if send_wakeup {
self.display.notify();
terminal.dirty = true;
}
......
This diff is collapsed.
This diff is collapsed.
// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and