added new gui to KubeInvaders

This commit is contained in:
Eugenio Marzo
2021-05-15 14:13:05 +02:00
parent 8e3164a528
commit 090f56dcd5
165 changed files with 82124 additions and 46 deletions
Vendored
+3 -3
View File
@@ -28,7 +28,7 @@ Vagrant.configure('2') do |config|
SCRIPT
config.vm.post_up_message = '\n\nEnjoy KubeInvaders!\n\n'
config.vm.synced_folder ".", "/vagrant", type: "smb"
#config.vm.synced_folder ".", "/vagrant", type: "smb"
config.vm.define 'kubeinvaders01' do |k3s|
k3s.vm.box = 'ubuntu/focal64'
@@ -37,8 +37,8 @@ Vagrant.configure('2') do |config|
k3s.vm.network 'private_network', ip: '192.168.58.99'
k3s.vm.network 'forwarded_port', guest: 80, host: 8080, host_ip: '127.0.0.1'
k3s.vm.provider :virtualbox do |vb|
vb.memory = 4096
vb.cpus = 2
vb.memory = 2048
vb.cpus = 1
end
#k3s.vm.provision "shell",
# run: "always",
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
echo "script for developing on OpenResty component of KubeInvaders"
docker build . -t kubeinvaders-dev
(docker ps | grep kubeinvaders-openresty) && sudo docker rm kubeinvaders-openresty -f
docker run -d -v $PWD/scripts:/usr/local/openresty/nginx/conf/kubeinvaders -p 8080:8080 \
--restart always --name kubeinvaders-openresty --env KUBERNETES_SERVICE_HOST=192.168.178.35 --env KUBERNETES_SERVICE_PORT=6443 --env ROUTE_HOST=192.168.178.35 --env ENDPOINT=192.168.178.35 --env K8S_TOKEN=$K8S_TOKEN --env NAMESPACE="namespace1,namespace2" kubeinvaders-dev
+31
View File
@@ -0,0 +1,31 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubeinvaders
labels:
app: kubeinvaders
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "watch", "list", "delete"]
- apiGroups: ["batch", "extensions"]
resources: ["jobs"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "watch", "list"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: kubeinvaders
labels:
app: kubeinvaders
subjects:
- kind: ServiceAccount
name: kubeinvaders
namespace: default
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: kubeinvaders
+6
View File
@@ -0,0 +1,6 @@
docker build . -t docker.io/luckysideburn/kubeinvaders:develop
docker push luckysideburn/kubeinvaders:develop
kubectl create namespace kubeinvaders
export KUBECONFIG=/etc/rancher/k3s/k3s.yaml
helm upgrade kubeinvaders --set-string target_namespace="namespace1\,namespace2" -n kubeinvaders ./helm-charts/kubeinvaders/ --set ingress.hostName=kubeinvaders.io --set image.tag=develop -i
kubectl delete pod --all -n kubeinvaders
+8 -3
View File
@@ -1,8 +1,13 @@
#!/bin/sh
if [ ! -z "$K8S_TOKEN" ];then
echo 'Found K8S_TOKEN... using K8S_TOKEN instead of TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)'
export TOKEN=$K8S_TOKEN
else
# Source the service account token from the container directly.
export TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
fi
# Source the service account token from the container directly.
export TOKEN="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
# TODO: use a sidecar
redis-server /etc/redis/redis.conf &
sed -i "s/ENDPOINT_PLACEHOLDER/$ENDPOINT/g" /var/www/html/kubeinvaders.js
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+426
View File
@@ -0,0 +1,426 @@
/*!
* Bootstrap Reboot v5.0.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.0.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
File diff suppressed because one or more lines are too long
+423
View File
@@ -0,0 +1,423 @@
/*!
* Bootstrap Reboot v5.0.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.0.0 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+107
View File
@@ -0,0 +1,107 @@
/*
* Base structure
*/
/* Move down content because we have a fixed navbar that is 50px tall */
body {
padding-top: 50px;
}
/*
* Global add-ons
*/
.sub-header {
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
/*
* Top navigation
* Hide default border to remove 1px line.
*/
.navbar-fixed-top {
border: 0;
}
/*
* Sidebar
*/
/* Hide for mobile, show later */
.sidebar {
display: none;
}
@media (min-width: 768px) {
.sidebar {
position: fixed;
top: 51px;
bottom: 0;
left: 0;
z-index: 1000;
display: block;
padding: 20px;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
background-color: #f5f5f5;
border-right: 1px solid #eee;
}
}
/* Sidebar navigation */
.nav-sidebar {
margin-right: -21px; /* 20px padding + 1px border */
margin-bottom: 20px;
margin-left: -20px;
}
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.nav-sidebar > .active > a,
.nav-sidebar > .active > a:hover,
.nav-sidebar > .active > a:focus {
color: #fff;
background-color: #428bca;
}
/*
* Main content
*/
.main {
padding: 20px;
}
@media (min-width: 768px) {
.main {
padding-right: 40px;
padding-left: 40px;
}
}
.main .page-header {
margin-top: 0;
}
/*
* Placeholder dashboard ideas
*/
.placeholders {
margin-bottom: 30px;
text-align: center;
}
.placeholders h4 {
margin-bottom: 0;
}
.placeholder {
margin-bottom: 20px;
}
.placeholder img {
display: inline-block;
border-radius: 50%;
}
@@ -0,0 +1,15 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
@-ms-viewport {
width: device-width;
}
@-o-viewport {
width: device-width;
}
@viewport {
width: device-width;
}
+8
View File
@@ -0,0 +1,8 @@
body {
padding-top: 50px;
}
.starter-template {
padding: 40px 15px;
text-align: center;
}
+81 -8
View File
@@ -1,16 +1,89 @@
<!DOCTYPE html>
<html>
<head>
<head>
<meta charset="utf-8" />
<title>KubeInvaders</title>
<!-- Bootstrap -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="./bootstrap-5.0.0-dist/css/bootstrap.min.css">
<style>
* { padding: 0; margin-top: 3%; background: #000;}
canvas { background: #000; display: block; margin: 0 auto; border:1px solid #00ff2a;}
canvas { background: #000; display: block; margin: 0 auto; border:1px solid #ffffff;}
</style>
</head>
<body>
<canvas id="myCanvas" width="720" height="480"></canvas>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">KubeInvaders</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDarkDropdown" aria-controls="navbarNavDarkDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavDarkDropdown">
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDarkDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Chaos Menu
</a>
<ul class="dropdown-menu dropdown-menu-dark" aria-labelledby="navbarDarkDropdownMenuLink">
<li><a class="dropdown-item" href="#" onclick="showCurrentChaosJob()">Show Current Chaos Job</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Modal -->
<div class="modal fade" id="currentChaosJobModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<pre id="currentChaosJobYaml"></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal" onclick="closeCurrentChaosJobModal()">Close</button>
</div>
</div>
</div>
</div>
<div class="container" style="width: 50%;">
<div class="starter-template">
<div class="row">
<img src="./logo.png" class="rounded mx-auto d-bloc" style="max-width: 25%; height: auto; margin-bottom: 0%;" alt="Responsive image">
</div>
<div class="row">
<canvas id="myCanvas" width="720" height="480"></canvas>
</div>
</div>
</div>
<div class="container" style="width: 50%; margin-top: 2%; margin-bottom: 2%;">
<div class="card">
<div class="card-header">
Special Keys
</div>
<div class="card-body">
<p class="card-text">
h => Activate or deactivate Help<br>
s => Activate or deactivate shuffle for aliens<br>
n => Change namespace<br>
p => Activate or deactivate chaos engineering against pods<br>
c => Activate or deactivate chaos engineering against nodes
</p>
</div>
</div>
</div>
<script src="kubeinvaders.js"></script>
</body>
<script src="./bootstrap-5.0.0-dist/js/bootstrap.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script>
function showCurrentChaosJob() {
getCurrentChaosJob();
$('#currentChaosJobModal').modal('show');
}
function closeCurrentChaosJobModal() {
$('#currentChaosJobModal').modal('hide');
}
</script>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
<!doctype html>
<html>
<head>
<title>Mohamed Elgharbawy</title>
<link rel="stylesheet" href="css/xterm.css"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xterm/3.14.5/xterm.min.js"></script>
<!-- Favicons -->
<link href="img/m.png" rel="icon">
<link href="img/m.png" rel="apple-touch-icon">
</head>
<body>
<div id="terminal"></div>
<script>
var term = new Terminal({
cursorBlink: "block"
});
var curr_line = '';
var entries = [];
var currPos = 0;
var pos = 0;
term.open(document.getElementById('terminal'));
term.prompt = () => {
term.write('\n\r' + curr_line + '\r\n\u001b[32mscm> \u001b[37m');
};
term.write('Welcome to my Scheme web intepreter!');
term.prompt();
term.on('key', function(key, ev) {
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey &&
!(ev.keyCode === 37 && term.buffer.cursorX < 6);
if (ev.keyCode === 13) { // Enter key
if (curr_line.replace(/^\s+|\s+$/g, '').length != 0) { // Check if string is all whitespace
entries.push(curr_line);
currPos = entries.length - 1;
term.prompt();
} else {
term.write('\n\33[2K\r\u001b[32mscm> \u001b[37m');
}
curr_line = '';
} else if (ev.keyCode === 8) { // Backspace
if (term.buffer.cursorX > 5) {
curr_line = curr_line.slice(0, term.buffer.cursorX - 6) + curr_line.slice(term.buffer.cursorX - 5);
pos = curr_line.length - term.buffer.cursorX + 6;
term.write('\33[2K\r\u001b[32mscm> \u001b[37m' + curr_line);
term.write('\033['.concat(pos.toString()).concat('D')); //term.write('\033[<N>D');
if (term.buffer.cursorX == 5 || term.buffer.cursorX == curr_line.length + 6) {
term.write('\033[1C')
}
}
} else if (ev.keyCode === 38) { // Up arrow
if (entries.length > 0) {
if (currPos > 0) {
currPos -= 1;
}
curr_line = entries[currPos];
term.write('\33[2K\r\u001b[32mscm> \u001b[37m' + curr_line);
}
} else if (ev.keyCode === 40) { // Down arrow
currPos += 1;
if (currPos === entries.length || entries.length === 0) {
currPos -= 1;
curr_line = '';
term.write('\33[2K\r\u001b[32mscm> \u001b[37m');
} else {
curr_line = entries[currPos];
term.write('\33[2K\r\u001b[32mscm> \u001b[37m' + curr_line);
}
} else if (printable && !(ev.keyCode === 39 && term.buffer.cursorX > curr_line.length + 4)) {
if (ev.keyCode != 37 && ev.keyCode != 39) {
var input = ev.key;
if (ev.keyCode == 9) { // Tab
input = " ";
}
pos = curr_line.length - term.buffer.cursorX + 4;
curr_line = [curr_line.slice(0, term.buffer.cursorX - 5), input, curr_line.slice(term.buffer.cursorX - 5)].join('');
term.write('\33[2K\r\u001b[32mscm> \u001b[37m' + curr_line);
term.write('\033['.concat(pos.toString()).concat('D')); //term.write('\033[<N>D');
} else {
term.write(key);
}
}
});
term.on('paste', function(data) {
curr_line += data;
term.write(curr_line);
});
</script>
</body></html>
+24
View File
@@ -0,0 +1,24 @@
/*!
* IE10 viewport hack for Surface/desktop Windows 8 bug
* Copyright 2014-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
// See the Getting Started docs for more information:
// https://getbootstrap.com/docs/3.4/getting-started/#support-ie10-width
(function () {
'use strict';
if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
var msViewportStyle = document.createElement('style')
msViewportStyle.appendChild(
document.createTextNode(
'@-ms-viewport{width:auto!important}'
)
)
document.querySelector('head').appendChild(msViewportStyle)
}
})();
+2
View File
File diff suppressed because one or more lines are too long
+58 -32
View File
@@ -71,6 +71,7 @@ var aliensIncrementY = 50;
var shuffle = true;
var help = false;
var chaos_nodes = true;
var chaos_pods = true;
function contains(a, obj) {
for (var i = 0; i < a.length; i++) {
@@ -81,7 +82,7 @@ function contains(a, obj) {
return false;
}
function getNamespaces(){
function getNamespaces() {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
namespaces = this.responseText;
@@ -93,7 +94,7 @@ function getNamespaces(){
}
function getEndpoint(){
function getEndpoint() {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
endpoint = this.responseText;
@@ -102,7 +103,19 @@ function getEndpoint(){
oReq.send();
}
function startChaosNode(node_name){
function getCurrentChaosJob() {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
console.log(this.responseText);
job_parsed = JSON.stringify(JSON.parse(this.responseText), null, 4);
console.log(job_parsed);
$('#currentChaosJobYaml').text(job_parsed);
};;
oReq.open("GET", "https://ENDPOINT_PLACEHOLDER/kube/chaos/containers?action=default");
oReq.send();
}
function startChaosNode(node_name) {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
//console.log(JSON.parse(this.responseText))
@@ -111,7 +124,7 @@ function startChaosNode(node_name){
oReq.send();
}
function deletePods(pod_name){
function deletePods(pod_name) {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
//console.log(JSON.parse(this.responseText))
@@ -120,21 +133,30 @@ function deletePods(pod_name){
oReq.send();
}
function getPods(){
var oReq = new XMLHttpRequest();
oReq.onload = function () {
json_parsed = JSON.parse(this.responseText);
if (nodes && nodes.length > 0){
pods = json_parsed["items"].concat(nodes);
function getPods() {
if (chaos_pods) {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
json_parsed = JSON.parse(this.responseText);
if (nodes && nodes.length > 0) {
pods = json_parsed["items"].concat(nodes);
} else {
pods = json_parsed["items"];
}
};;
oReq.open("GET", "https://ENDPOINT_PLACEHOLDER/kube/pods?action=list&namespace=" + namespace);
oReq.send();
}
else {
if (nodes && nodes.length > 0) {
pods = nodes;
} else {
pods = json_parsed["items"];
}
};;
oReq.open("GET", "https://ENDPOINT_PLACEHOLDER/kube/pods?action=list&namespace=" + namespace);
oReq.send();
pods = [];
}
}
}
function getNodes(){
function getNodes() {
if (chaos_nodes) {
var oReq = new XMLHttpRequest();
oReq.onload = function () {
@@ -221,6 +243,14 @@ function keyDownHandler(e) {
chaos_nodes = true
}
}
else if(e.keyCode == 80) {
if (chaos_pods) {
chaos_pods = false;
}
else {
chaos_pods = true
}
}
}
function keyUpHandler(e) {
@@ -255,8 +285,8 @@ function drawAlien(alienX, alienY, name) {
ctx.closePath();
}
function checkRocketAlienCollision(){
if (contains(aliensY, rocketY)){
function checkRocketAlienCollision() {
if (contains(aliensY, rocketY)) {
//console.log("The y of rocket is the same of an alien. rocketY=" + rocketY + " List of aliensY:" + aliensY);
var i;
for (i=aliens.length - 1; i >= 0; i--) {
@@ -308,7 +338,7 @@ function drawRocket() {
}
if(shot && rocketLaunched) {
if (rocketY < 0){
if (rocketY < 0) {
//console.log("Rocket arrived to the end of canvas");
shot = false;
rocketLaunched = false;
@@ -332,7 +362,8 @@ function drawSpaceship() {
}
window.setInterval(function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.clearRect(0, 0, canvas.widthWith supporting text below as a natural lead-in to additional content
, canvas.height);
drawSpaceship();
if (shot && !collisionDetected) {
@@ -361,7 +392,7 @@ window.setInterval(function draw() {
if(upPressed) {
spaceshipY -= 3;
if (spaceshipY < 0){
if (spaceshipY < 0) {
spaceshipY = 0;
}
}
@@ -386,16 +417,11 @@ window.setInterval(function draw() {
ctx.fillText('press \'h\' for help!', 10, 470);
if (help) {
ctx.fillText('Special Keys:', 10, 300);
ctx.fillText('h => Activate or deactivate Help', 10, 320);
ctx.fillText('s => Activate or deactivate shuffle for aliens', 10, 340);
ctx.fillText('n => Change namespace', 10, 360);
ctx.fillText('c => Activate or deactivate chaos engineering against nodes', 10, 380);
}
}, 10)
function podExists(podName) {
for (i=0; i<aliens.length; i++) {
ctx.fillText('Special Keys:', 10, 280);
ctx.fillText('h => Activate or deactivate Help', 10, 300);
ctx.fillText('s => Activate or deactivate shuffle for aliens', 10, 320);
ctx.fillText('n => Change namespace', 10, 340);
ctx.fillText('p => Activate or deacdes
if (aliens[i]["name"] == podName) {
return true;
}
@@ -457,4 +483,4 @@ window.setInterval(function setAliens() {
}, 1000)
getEndpoint();
getNamespaces();
getNamespaces();
Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+164
View File
@@ -0,0 +1,164 @@
{
"env": {
"browser": true,
"es6": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"test/api/tsconfig.json",
"test/benchmark/tsconfig.json",
"addons/xterm-addon-attach/src/tsconfig.json",
"addons/xterm-addon-attach/test/tsconfig.json",
"addons/xterm-addon-fit/src/tsconfig.json",
"addons/xterm-addon-fit/test/tsconfig.json",
"addons/xterm-addon-ligatures/src/tsconfig.json",
"addons/xterm-addon-search/src/tsconfig.json",
"addons/xterm-addon-search/test/tsconfig.json",
"addons/xterm-addon-serialize/src/tsconfig.json",
"addons/xterm-addon-serialize/test/tsconfig.json",
"addons/xterm-addon-serialize/benchmark/tsconfig.json",
"addons/xterm-addon-unicode11/src/tsconfig.json",
"addons/xterm-addon-unicode11/test/tsconfig.json",
"addons/xterm-addon-web-links/src/tsconfig.json",
"addons/xterm-addon-web-links/test/tsconfig.json",
"addons/xterm-addon-webgl/src/tsconfig.json",
"addons/xterm-addon-webgl/test/tsconfig.json"
],
"sourceType": "module"
},
"ignorePatterns": [
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js"
],
"plugins": [
"@typescript-eslint"
],
"rules": {
"no-extra-semi": "error",
"@typescript-eslint/array-type": [
"warn",
{
"default": "array-simple",
"readonly": "generic"
}
],
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/explicit-member-accessibility": [
"warn",
{
"accessibility": "explicit",
"overrides": {
"constructors": "off"
}
}
],
"@typescript-eslint/indent": [
"warn",
2
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"warn",
{ "selector": "default", "format": ["camelCase"] },
// variableLike
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
// memberLike
{ "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
// typeLike
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
],
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"@typescript-eslint/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/semi": [
"warn",
"always"
],
"comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"curly": [
"warn",
"multi-line"
],
"eol-last": "warn",
"eqeqeq": [
"warn",
"always"
],
"keyword-spacing": "warn",
"new-parens": "warn",
"no-duplicate-imports": "warn",
"no-else-return": [
"warn",
{
"allowElseIf": false
}
],
"no-eval": "warn",
"no-irregular-whitespace": "warn",
"no-restricted-imports": [
"warn",
{
"patterns": [
".*\\/out\\/.*"
]
}
],
"no-trailing-spaces": "warn",
"no-unsafe-finally": "warn",
"no-var": "warn",
"one-var": [
"warn",
"never"
],
"prefer-const": "warn",
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
}
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+205
View File
@@ -0,0 +1,205 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
## Features
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Rich unicode support**: Supports CJK, emojis and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on
- **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
## Getting Started
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
```
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
```html
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
<script src="node_modules/xterm/lib/xterm.js"></script>
</head>
<body>
<div id="terminal"></div>
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
```
### Importing
The recommended way to load xterm.js is via the ES6 module syntax:
```javascript
import { Terminal } from 'xterm';
```
### Addons
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project:
```bash
npm i -S xterm-addon-web-links
```
Then import the addon, instantiate it and call `Terminal.loadAddon`:
```ts
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
const terminal = new Terminal();
// Load WebLinksAddon on terminal, this is all that's needed to get web links
// working in the terminal.
terminal.loadAddon(new WebLinksAddon());
```
The xterm.js team maintains the following addons but they can be built by anyone:
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality
- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
## API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom.
- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017.
- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React.
- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm.
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS.
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux.
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users.
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD
- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access.
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `<ng-terminal></ng-terminal>` into your component.
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
- [**Gus**](https://gus.jp): A shared coding pad where you can run Python with xterm.js
- [**Linode**](https://linode.com): Linode uses xterm.js to provide users a web console for their Linode instances.
- [**FluffOS**](https://www.fluffos.info): Active maintained LPMUD driver with websocket support.
- [**x-terminal**](https://atom.io/packages/x-terminal): Atom plugin for providing terminals inside your Atom workspace.
- [**CoCalc**](https://cocalc.com/): Lots of free software pre-installed, to chat, collaborate, develop, program, publish, research, share, teach, in C++, HTML, Julia, Jupyter, LaTeX, Markdown, Python, R, SageMath, Scala, ...
- [**Dank Domain**](https://www.DDgame.us/): Open source multiuser medieval game supporting old & new terminal emulation.
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services.
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
## Releases
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
```
npm install -S xterm@beta
```
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+171
View File
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* https://github.com/chjj/term.js
* @license MIT
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* Originally forked from (with the author's permission):
* Fabrice Bellard's javascript vt100 for jslinux:
* http://bellard.org/jslinux/
* Copyright (c) 2011 Fabrice Bellard
* The original design remains. The terminal itself
* has been extended to include xterm CSI codes, among
* other features.
*/
/**
* Default styles for xterm.js
*/
.xterm {
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.xterm.focus,
.xterm:focus {
outline: none;
}
.xterm .xterm-helpers {
position: absolute;
top: 0;
/**
* The z-index of the helpers must be higher than the canvases in order for
* IMEs to appear on top.
*/
z-index: 5;
}
.xterm .xterm-helper-textarea {
padding: 0;
border: 0;
margin: 0;
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
position: absolute;
opacity: 0;
left: -9999em;
top: 0;
width: 0;
height: 0;
z-index: -5;
/** Prevent wrapping so the IME appears against the textarea at the correct position */
white-space: nowrap;
overflow: hidden;
resize: none;
}
.xterm .composition-view {
/* TODO: Composition position got messed up somewhere */
background: #000;
color: #FFF;
display: none;
position: absolute;
white-space: nowrap;
z-index: 1;
}
.xterm .composition-view.active {
display: block;
}
.xterm .xterm-viewport {
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
}
.xterm .xterm-scroll-area {
visibility: hidden;
}
.xterm-char-measure-element {
display: inline-block;
visibility: hidden;
position: absolute;
top: 0;
left: -9999em;
line-height: normal;
}
.xterm {
cursor: text;
}
.xterm.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.xterm.xterm-cursor-pointer {
cursor: pointer;
}
.xterm.column-select.focus {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
.xterm .live-region {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}
.xterm-dim {
opacity: 0.5;
}
.xterm-underline {
text-decoration: underline;
}
+2
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+103
View File
@@ -0,0 +1,103 @@
{
"_from": "xterm",
"_id": "xterm@4.11.0",
"_inBundle": false,
"_integrity": "sha512-NeJH909WTO2vth/ZlC0gkP3AGzupbvVHVlmtrpBw56/sGFXaF9bNdKgqKa3tf8qbGvXMzL2JhCcHVklqFztIRw==",
"_location": "/xterm",
"_phantomChildren": {},
"_requested": {
"type": "tag",
"registry": true,
"raw": "xterm",
"name": "xterm",
"escapedName": "xterm",
"rawSpec": "",
"saveSpec": null,
"fetchSpec": "latest"
},
"_requiredBy": [
"#USER",
"/"
],
"_resolved": "https://registry.npmjs.org/xterm/-/xterm-4.11.0.tgz",
"_shasum": "d7dabc7af5299579e4663fedf2b3a179af9aaff9",
"_spec": "xterm",
"_where": "/home/eugenio/KubeInvaders",
"bugs": {
"url": "https://github.com/xtermjs/xterm.js/issues"
},
"bundleDependencies": false,
"deprecated": false,
"description": "Full xterm terminal, in your browser",
"devDependencies": {
"@types/chai": "^4.2.14",
"@types/debug": "^4.1.5",
"@types/deep-equal": "^1.0.1",
"@types/glob": "^7.1.3",
"@types/jsdom": "^16.2.5",
"@types/mocha": "^8.0.3",
"@types/node": "^10.17.17",
"@types/utf8": "^2.1.6",
"@types/webpack": "^4.41.24",
"@types/ws": "^7.2.9",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^3.10.1",
"chai": "^4.2.0",
"deep-equal": "^2.0.4",
"eslint": "^7.12.1",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"glob": "^7.0.5",
"jsdom": "^16.4.0",
"mocha": "^8.2.1",
"mustache": "^4.0.1",
"node-pty": "^0.9.0",
"nyc": "^15.1.0",
"playwright": "^1.5.2",
"source-map-loader": "^1.1.2",
"ts-loader": "^8.0.8",
"typescript": "4.0",
"utf8": "^3.0.0",
"webpack": "^5.4.0",
"webpack-cli": "^4.2.0",
"ws": "^7.3.1",
"xterm-benchmark": "^0.1.3"
},
"homepage": "https://github.com/xtermjs/xterm.js#readme",
"license": "MIT",
"main": "lib/xterm.js",
"name": "xterm",
"repository": {
"type": "git",
"url": "git+https://github.com/xtermjs/xterm.js.git"
},
"scripts": {
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/*benchmark.js",
"build": "tsc -b ./tsconfig.all.json",
"clean": "rm -rf lib out addons/*/lib addons/*/out",
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
"package": "webpack",
"posttest": "npm run lint",
"prepackage": "npm run build",
"prepare": "npm run setup",
"prepublishOnly": "npm run package",
"presetup": "node ./bin/install-addons.js",
"setup": "npm run build",
"start": "node demo/start",
"start-debug": "node --inspect-brk demo/start",
"test": "npm run test-unit",
"test-api": "npm run test-api-chromium",
"test-api-chromium": "node ./bin/test_api.js --browser=chromium --timeout=20000",
"test-api-firefox": "node ./bin/test_api.js --browser=firefox --timeout=20000",
"test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000",
"test-unit": "node ./bin/test.js",
"test-unit-coverage": "node ./bin/test.js --coverage",
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts",
"watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput"
},
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
"version": "4.11.0"
}
+300
View File
@@ -0,0 +1,300 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Strings from 'browser/LocalizableStrings';
import { ITerminal } from 'browser/Types';
import { IBuffer } from 'common/buffer/Types';
import { isMac } from 'common/Platform';
import { RenderDebouncer } from 'browser/RenderDebouncer';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { Disposable } from 'common/Lifecycle';
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
import { IRenderService } from 'browser/services/Services';
import { removeElementFromParent } from 'browser/Dom';
const MAX_ROWS_TO_READ = 20;
const enum BoundaryPosition {
TOP,
BOTTOM
}
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _screenDprMonitor: ScreenDprMonitor;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
* not announced (added to live region) because it has already been announced
* by the textarea event (which cannot be canceled). There are some race
* condition cases if there is typing while data is streaming, but this covers
* the main case of typing into the prompt and inputting the answer to a
* question (Y/N, etc.).
*/
private _charsToConsume: string[] = [];
private _charsToAnnounce: string = '';
constructor(
private readonly _terminal: ITerminal,
private readonly _renderService: IRenderService
) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.setAttribute('role', 'list');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
if (!this._terminal.element) {
throw new Error('Cannot enable accessibility before Terminal.open');
}
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this.register(this._renderRowsDebouncer);
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
this.register(this._terminal.onScroll(() => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this.register(this._terminal.onA11yChar(char => this._onChar(char)));
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.onKey(e => this._onKey(e.key)));
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
this._screenDprMonitor = new ScreenDprMonitor();
this.register(this._screenDprMonitor);
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the ScreenDprMonitor isn't supported
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
super.dispose();
removeElementFromParent(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.TOP) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop()!;
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift()!;
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.TOP) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _onResize(rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _onTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
}
}
private _onChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._charsToAnnounce += char;
}
} else {
this._charsToAnnounce += char;
}
if (char === '\n') {
this._liveRegionLineCount++;
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
this._liveRegion.textContent += Strings.tooMuchOutput;
}
}
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
}, 0);
}
}
}
}
private _clearLiveRegion(): void {
this._liveRegion.textContent = '';
this._liveRegionLineCount = 0;
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
removeElementFromParent(this._liveRegion);
}
}
private _onKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
if (element) {
if (lineData.length === 0) {
element.innerText = '\u00a0';
} else {
element.textContent = lineData;
}
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
this._announceCharacters();
}
private _refreshRowsDimensions(): void {
if (!this._renderService.dimensions.actualCellHeight) {
return;
}
if (this._rowElements.length !== this._terminal.rows) {
this._onResize(this._terminal.rows);
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._renderService.dimensions.actualCellHeight}px`;
}
private _announceCharacters(): void {
if (this._charsToAnnounce.length === 0) {
return;
}
this._liveRegion.textContent += this._charsToAnnounce;
this._charsToAnnounce = '';
}
}
+99
View File
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ISelectionService } from 'browser/services/Services';
import { ICoreService } from 'common/services/Services';
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForTerminal(text: string): string {
return text.replace(/\r?\n/g, '\r');
}
/**
* Bracket text for paste, if necessary, as per https://cirw.in/blog/bracketed-paste
* @param text The pasted text to bracket
*/
export function bracketTextForPaste(text: string, bracketedPasteMode: boolean): string {
if (bracketedPasteMode) {
return '\x1b[200~' + text + '\x1b[201~';
}
return text;
}
/**
* Binds copy functionality to the given terminal.
* @param ev The original copy event to be handled
*/
export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionService): void {
if (ev.clipboardData) {
ev.clipboardData.setData('text/plain', selectionService.selectionText);
}
// Prevent or the original text will be copied.
ev.preventDefault();
}
/**
* Redirect the clipboard's data to the terminal's input handler.
* @param ev The original paste event to be handled
* @param term The terminal on which to apply the handled paste event
*/
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
ev.stopPropagation();
if (ev.clipboardData) {
const text = ev.clipboardData.getData('text/plain');
paste(text, textarea, coreService);
}
}
export function paste(text: string, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, coreService.decPrivateModes.bracketedPasteMode);
coreService.triggerDataEvent(text, true);
textarea.value = '';
}
/**
* Moves the textarea under the mouse cursor and focuses it.
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
*/
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement): void {
// Calculate textarea position relative to the screen element
const pos = screenElement.getBoundingClientRect();
const left = ev.clientX - pos.left - 10;
const top = ev.clientY - pos.top - 10;
// Bring textarea at the cursor position
textarea.style.width = '20px';
textarea.style.height = '20px';
textarea.style.left = `${left}px`;
textarea.style.top = `${top}px`;
textarea.style.zIndex = '1000';
textarea.focus();
}
/**
* Bind to right-click event and allow right-click copy and paste.
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionService The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
if (shouldSelectWord) {
selectionService.rightClickSelect(ev);
}
// Get textarea ready to copy from the context menu
textarea.value = selectionService.selectionText;
textarea.select();
}
+228
View File
@@ -0,0 +1,228 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor } from 'browser/Types';
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
export namespace channels {
export function toCss(r: number, g: number, b: number, a?: number): string {
if (a !== undefined) {
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
}
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
}
/**
* Helper functions where the source type is `IColor`.
*/
export namespace color {
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
return {
css: fg.css,
rgba: fg.rgba
};
}
const fgR = (fg.rgba >> 24) & 0xFF;
const fgG = (fg.rgba >> 16) & 0xFF;
const fgB = (fg.rgba >> 8) & 0xFF;
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = channels.toCss(r, g, b);
const rgba = channels.toRgba(r, g, b);
return { css, rgba };
}
export function isOpaque(color: IColor): boolean {
return (color.rgba & 0xFF) === 0xFF;
}
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgba.toColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
export function opaque(color: IColor): IColor {
const rgbaColor = (color.rgba | 0xFF) >>> 0;
const [r, g, b] = rgba.toChannels(rgbaColor);
return {
css: channels.toCss(r, g, b),
rgba: rgbaColor
};
}
export function opacity(color: IColor, opacity: number): IColor {
const a = Math.round(opacity * 0xFF);
const [r, g, b] = rgba.toChannels(color.rgba);
return {
css: channels.toCss(r, g, b, a),
rgba: channels.toRgba(r, g, b, a)
};
}
}
/**
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
*/
export namespace css {
export function toColor(css: string): IColor {
switch (css.length) {
case 7: // #rrggbb
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
case 9: // #rrggbbaa
return {
css,
rgba: parseInt(css.slice(1), 16) >>> 0
};
}
throw new Error('css.toColor: Unsupported css format');
}
}
/**
* Helper functions where the source type is "rgb" (number: 0xrrggbb).
*/
export namespace rgb {
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param rgb The color to use.
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function relativeLuminance(rgb: number): number {
return relativeLuminance2(
(rgb >> 16) & 0xFF,
(rgb >> 8 ) & 0xFF,
(rgb ) & 0xFF);
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param r The red channel (0x00 to 0xFF).
* @param g The green channel (0x00 to 0xFF).
* @param b The blue channel (0x00 to 0xFF).
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function relativeLuminance2(r: number, g: number, b: number): number {
const rs = r / 255;
const gs = g / 255;
const bs = b / 255;
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
}
}
/**
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgb.relativeLuminance(bgRgba >> 8);
const fgL = rgb.relativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bgRgba, fgRgba, ratio);
}
return increaseLuminance(bgRgba, fgRgba, ratio);
}
return undefined;
}
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to reducing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Reduce by 10% until the ratio is hit
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to increasing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
// Increase by 10% until the ratio is hit
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function toChannels(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function toColor(r: number, g: number, b: number): IColor {
return {
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
};
}
}
export function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
/**
* Gets the contrast ratio between two relative luminance values.
* @param l1 The first relative luminance.
* @param l2 The first relative luminance.
* @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
*/
export function contrastRatio(l1: number, l2: number): number {
if (l1 < l2) {
return (l2 + 0.05) / (l1 + 0.05);
}
return (l1 + 0.05) / (l2 + 0.05);
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor, IColorContrastCache } from 'browser/Types';
export class ColorContrastCache implements IColorContrastCache {
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
public clear(): void {
this._color = {};
this._rgba = {};
}
public setCss(bg: number, fg: number, value: string | null): void {
if (!this._rgba[bg]) {
this._rgba[bg] = {};
}
this._rgba[bg]![fg] = value;
}
public getCss(bg: number, fg: number): string | null | undefined {
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
}
public setColor(bg: number, fg: number, value: IColor | null): void {
if (!this._color[bg]) {
this._color[bg] = {};
}
this._color[bg]![fg] = value;
}
public getColor(bg: number, fg: number): IColor | null | undefined {
return this._color[bg] ? this._color[bg]![fg] : undefined;
}
}
+213
View File
@@ -0,0 +1,213 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'browser/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
const DEFAULT_BACKGROUND = css.toColor('#000000');
const DEFAULT_CURSOR = css.toColor('#ffffff');
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF4D
};
// An IIFE to generate DEFAULT_ANSI_COLORS.
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
const colors = [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
// Generate colors (16-231)
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
colors.push({
css: channels.toCss(c, c, c),
rgba: channels.toRgba(c, c, c)
});
}
return colors;
})());
/**
* Manages the source of truth for a terminal's colors.
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
private _contrastCache: IColorContrastCache;
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Could not get rendering context');
}
this._ctx = ctx;
this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
this._contrastCache = new ColorContrastCache();
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selectionTransparent: DEFAULT_SELECTION,
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
}
public onOptionsChange(key: string): void {
if (key === 'minimumContrastRatio') {
this._contrastCache.clear();
}
}
/**
* Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme = {}): void {
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selectionTransparent = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selectionTransparent);
/**
* If selection color is opaque, blend it with background with 0.3 opacity
* Issue #2737
*/
if (color.isOpaque(this.colors.selectionTransparent)) {
const opacity = 0.3;
this.colors.selectionTransparent = color.opacity(this.colors.selectionTransparent, opacity);
}
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
// Clear our the cache
this._contrastCache.clear();
}
private _parseColor(
css: string | undefined,
fallback: IColor,
allowTransparency: boolean = this.allowTransparency
): IColor {
if (css === undefined) {
return fallback;
}
// If parsing the value results in failure, then it must be ignored, and the attribute must
// retain its previous value.
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
this._ctx.fillStyle = this._litmusColor;
this._ctx.fillStyle = css;
if (typeof this._ctx.fillStyle !== 'string') {
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
return fallback;
}
this._ctx.fillRect(0, 0, 1, 1);
const data = this._ctx.getImageData(0, 0, 1, 1).data;
// Check if the printed color was transparent
if (data[3] !== 0xFF) {
if (!allowTransparency) {
// Ideally we'd just ignore the alpha channel, but...
//
// Browsers may not give back exactly the same RGB values we put in, because most/all
// convert the color to a pre-multiplied representation. getImageData converts that back to
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
// on their own.
//
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
// into 0x00000000.
//
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
// pixels that have just been set using putImageData() might be returned to an equivalent
// getImageData() as different values."
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
//
// So let's just use the fallback color in this case instead.
console.warn(
`Color: ${css} is using transparency, but allowTransparency is false. ` +
`Using fallback ${fallback.css}.`
);
return fallback;
}
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// the color value has alpha less than 1.0, and the string is the color value in the CSS rgba()
const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component));
const alpha = Math.round(a * 255);
const rgba: number = channels.toRgba(r, g, b, alpha);
return {
rgba,
css
};
}
return {
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character
css: this._ctx.fillStyle,
rgba: channels.toRgba(data[0], data[1], data[2], data[3])
};
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function removeElementFromParent(...elements: (HTMLElement | undefined)[]): void {
for (const e of elements) {
e?.parentElement?.removeChild(e);
}
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
/**
* Adds a disposable listener to a node in the DOM, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableDomListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
options?: boolean | AddEventListenerOptions
): IDisposable {
node.addEventListener(type, handler, options);
let disposed = false;
return {
dispose: () => {
if (disposed) {
return;
}
disposed = true;
node.removeEventListener(type, handler, options);
}
};
}
+354
View File
@@ -0,0 +1,354 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types';
import { IBufferStringIteratorResult } from 'common/buffer/Types';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ILogService, IBufferService, IOptionsService, IUnicodeService } from 'common/services/Services';
/**
* Limit of the unwrapping line expansion (overscan) at the top and bottom
* of the actual viewport in ASCII characters.
* A limit of 2000 should match most sane urls.
*/
const OVERSCAN_CHAR_LIMIT = 2000;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
export class Linkifier implements ILinkifier {
/**
* The time to wait after a row is changed before it is linkified. This prevents
* the costly operation of searching every row multiple times, potentially a
* huge amount of times.
*/
protected static _timeBeforeLatency = 200;
protected _linkMatchers: IRegisteredLinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager | undefined;
private _element: HTMLElement | undefined;
private _rowsTimeoutId: number | undefined;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: { start: number | undefined, end: number | undefined };
private _onShowLinkUnderline = new EventEmitter<ILinkifierEvent>();
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
private _onHideLinkUnderline = new EventEmitter<ILinkifierEvent>();
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
private _onLinkTooltip = new EventEmitter<ILinkifierEvent>();
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
constructor(
@IBufferService protected readonly _bufferService: IBufferService,
@ILogService private readonly _logService: ILogService,
@IUnicodeService private readonly _unicodeService: IUnicodeService
) {
this._rowsToLinkify = {
start: undefined,
end: undefined
};
}
/**
* Attaches the linkifier to the DOM, enabling linkification.
* @param mouseZoneManager The mouse zone manager to register link zones with.
*/
public attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void {
this._element = element;
this._mouseZoneManager = mouseZoneManager;
}
/**
* Queue linkification on a set of rows.
* @param start The row to linkify from (inclusive).
* @param end The row to linkify to (inclusive).
*/
public linkifyRows(start: number, end: number): void {
// Don't attempt linkify if not yet attached to DOM
if (!this._mouseZoneManager) {
return;
}
// Increase range to linkify
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
this._rowsToLinkify.start = start;
this._rowsToLinkify.end = end;
} else {
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start);
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end);
}
// Clear out any existing links on this row range
this._mouseZoneManager.clearAll(start, end);
// Restart timer
if (this._rowsTimeoutId) {
clearTimeout(this._rowsTimeoutId);
}
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency);
}
/**
* Linkifies the rows requested.
*/
private _linkifyRows(): void {
this._rowsTimeoutId = undefined;
const buffer = this._bufferService.buffer;
if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) {
this._logService.debug('_rowToLinkify was unset before _linkifyRows was called');
return;
}
// Ensure the start row exists
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
if (absoluteRowIndexStart >= buffer.lines.length) {
return;
}
// Invalidate bad end row values (if a resize happened)
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._bufferService.rows) + 1;
// Iterate over the range of unwrapped content strings within start..end
// (excluding).
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
// for every matcher.
// The unwrapping is needed to also match content that got wrapped across
// several buffer lines. To avoid a worst case scenario where the whole buffer
// contains just a single unwrapped string we limit this line expansion beyond
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
// chars will not match anymore at the viewport borders.
const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._bufferService.cols);
const iterator = this._bufferService.buffer.iterator(
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
while (iterator.hasNext()) {
const lineData: IBufferStringIteratorResult = iterator.next();
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
}
}
this._rowsToLinkify.start = undefined;
this._rowsToLinkify.end = undefined;
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
* @param regex The regular expression to search for. Specifically, this
* searches the textContent of the rows. You will want to use \s to match a
* space ' ' character for example.
* @param handler The callback when the link is called.
* @param options Options for the link matcher.
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (!handler) {
throw new Error('handler must be defined');
}
const matcher: IRegisteredLinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
matchIndex: options.matchIndex,
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
return matcher.id;
}
/**
* Inserts a link matcher to the list in the correct position based on the
* priority of each link matcher. New link matchers of equal priority are
* considered after older link matchers.
* @param matcher The link matcher to be added.
*/
private _addLinkMatcherToList(matcher: IRegisteredLinkMatcher): void {
if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher);
return;
}
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
if (matcher.priority <= this._linkMatchers[i].priority) {
this._linkMatchers.splice(i + 1, 0, matcher);
return;
}
}
this._linkMatchers.splice(0, 0, matcher);
}
/**
* Deregisters a link matcher if it has been registered.
* @param matcherId The link matcher's ID (returned after register)
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
}
}
return false;
}
/**
* Linkifies a row given a specific handler.
* @param rowIndex The row index to linkify (absolute index).
* @param text string content of the unwrapped row.
* @param matcher The link matcher for this line.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
// clone regex to do a global search on text
const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g');
let match;
let stringIndex = -1;
while ((match = rex.exec(text)) !== null) {
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
if (!uri) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
this._logService.debug('match found without corresponding matchIndex', match, matcher);
break;
}
// Get index, match.index is for the outer match which includes negated chars
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = text.indexOf(uri, stringIndex + 1);
rex.lastIndex = stringIndex + uri.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
}
// get the buffer index as [absolute row, col] for the match
const bufferIndex = this._bufferService.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
if (bufferIndex[0] < 0) {
// invalid bufferIndex (should not have happened)
break;
}
const line = this._bufferService.buffer.lines.get(bufferIndex[0]);
if (!line) {
break;
}
const attr = line.getFg(bufferIndex[1]);
const fg = attr ? (attr >> 9) & 0x1ff : undefined;
if (matcher.validationCallback) {
matcher.validationCallback(uri, isValid => {
// Discard link if the line has already changed
if (this._rowsTimeoutId) {
return;
}
if (isValid) {
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
}
});
} else {
this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg);
}
}
}
/**
* Registers a link to the mouse zone manager.
* @param x The column the link starts.
* @param y The row the link is on.
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
* @param fg The link color for hover event.
*/
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number | undefined): void {
if (!this._mouseZoneManager || !this._element) {
return;
}
// FIXME: get cell length from buffer to avoid mismatch after Unicode version change
const width = this._unicodeService.getStringCellWidth(uri);
const x1 = x % this._bufferService.cols;
const y1 = y + Math.floor(x / this._bufferService.cols);
let x2 = (x1 + width) % this._bufferService.cols;
let y2 = y1 + Math.floor((x1 + width) / this._bufferService.cols);
if (x2 === 0) {
x2 = this._bufferService.cols;
y2--;
}
this._mouseZoneManager.add(new MouseZone(
x1 + 1,
y1 + 1,
x2 + 1,
y2 + 1,
e => {
if (matcher.handler) {
return matcher.handler(e, uri);
}
const newWindow = window.open();
if (newWindow) {
newWindow.opener = null;
newWindow.location.href = uri;
} else {
console.warn('Opening link blocked as opener could not be cleared');
}
},
() => {
this._onShowLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._element!.classList.add('xterm-cursor-pointer');
},
e => {
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) {
// Note that IViewportRange use 1-based coordinates to align with escape sequences such
// as CUP which use 1,1 as the default for row/col
matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } });
}
},
() => {
this._onHideLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
this._element!.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
}
return true;
}
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
return { x1, y1, x2, y2, cols: this._bufferService.cols, fg };
}
}
export class MouseZone implements IMouseZone {
constructor(
public x1: number,
public y1: number,
public x2: number,
public y2: number,
public clickCallback: (e: MouseEvent) => any,
public hoverCallback: (e: MouseEvent) => any,
public tooltipCallback: (e: MouseEvent) => any,
public leaveCallback: () => void,
public willLinkActivate: (e: MouseEvent) => boolean
) {
}
}
+401
View File
@@ -0,0 +1,401 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
import { IDisposable } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
interface ILinkState {
decorations: ILinkDecorations;
isHovered: boolean;
}
interface ILinkWithState {
link: ILink;
state?: ILinkState;
}
export class Linkifier2 extends Disposable implements ILinkifier2 {
private _element: HTMLElement | undefined;
private _mouseService: IMouseService | undefined;
private _renderService: IRenderService | undefined;
private _linkProviders: ILinkProvider[] = [];
protected _currentLink: ILinkWithState | undefined;
private _lastMouseEvent: MouseEvent | undefined;
private _linkCacheDisposables: IDisposable[] = [];
private _lastBufferCell: IBufferCellPosition | undefined;
private _isMouseOut: boolean = true;
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
private _activeLine: number = -1;
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
private _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
constructor(
@IBufferService private readonly _bufferService: IBufferService
) {
super();
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._linkProviders.push(linkProvider);
return {
dispose: () => {
// Remove the link provider from the list
const providerIndex = this._linkProviders.indexOf(linkProvider);
if (providerIndex !== -1) {
this._linkProviders.splice(providerIndex, 1);
}
}
};
}
public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void {
this._element = element;
this._mouseService = mouseService;
this._renderService = renderService;
this.register(addDisposableDomListener(this._element, 'mouseleave', () => {
this._isMouseOut = true;
this._clearCurrentLink();
}));
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
this.register(addDisposableDomListener(this._element, 'click', this._onClick.bind(this)));
}
private _onMouseMove(event: MouseEvent): void {
this._lastMouseEvent = event;
if (!this._element || !this._mouseService) {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
}
this._isMouseOut = false;
// Ignore the event if it's an embedder created hover widget
const composedPath = event.composedPath() as HTMLElement[];
for (let i = 0; i < composedPath.length; i++) {
const target = composedPath[i];
// Hit Terminal.element, break and continue
if (target.classList.contains('xterm')) {
break;
}
// It's a hover, don't respect hover event
if (target.classList.contains('xterm-hover')) {
return;
}
}
if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) {
this._onHover(position);
this._lastBufferCell = position;
}
}
private _onHover(position: IBufferCellPosition): void {
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
// Check if we need to clear the link
if (this._activeLine !== position.y) {
this._clearCurrentLink();
this._askForLink(position, false);
return;
}
// Check the if the link is in the mouse position
const isCurrentLinkInPosition = this._currentLink && this._linkAtPosition(this._currentLink.link, position);
if (!isCurrentLinkInPosition) {
this._clearCurrentLink();
this._askForLink(position, true);
}
}
private _askForLink(position: IBufferCellPosition, useLineCache: boolean): void {
if (!this._activeProviderReplies || !useLineCache) {
this._activeProviderReplies?.forEach(reply => {
reply?.forEach(linkWithState => {
if (linkWithState.link.dispose) {
linkWithState.link.dispose();
}
});
});
this._activeProviderReplies = new Map();
this._activeLine = position.y;
}
let linkProvided = false;
// There is no link cached, so ask for one
this._linkProviders.forEach((linkProvider, i) => {
if (useLineCache) {
const existingReply = this._activeProviderReplies?.get(i);
// If there isn't a reply, the provider hasn't responded yet.
// TODO: If there isn't a reply yet it means that the provider is still resolving. Ensuring
// provideLinks isn't triggered again saves ILink.hover firing twice though. This probably
// needs promises to get fixed
if (existingReply) {
linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
}
} else {
linkProvider.provideLinks(position.y, (links: ILink[] | undefined) => {
if (this._isMouseOut) {
return;
}
const linksWithState: ILinkWithState[] | undefined = links?.map(link => ({ link }));
this._activeProviderReplies?.set(i, linksWithState);
linkProvided = this._checkLinkProviderResult(i, position, linkProvided);
// If all providers have responded, remove lower priority links that intersect ranges of
// higher priority links
if (this._activeProviderReplies?.size === this._linkProviders.length) {
this._removeIntersectingLinks(position.y, this._activeProviderReplies);
}
});
}
});
}
private _removeIntersectingLinks(y: number, replies: Map<Number, ILinkWithState[] | undefined>): void {
const occupiedCells = new Set<number>();
for (let i = 0; i < replies.size; i++) {
const providerReply = replies.get(i);
if (!providerReply) {
continue;
}
for (let i = 0; i < providerReply.length; i++) {
const linkWithState = providerReply[i];
const startX = linkWithState.link.range.start.y < y ? 0 : linkWithState.link.range.start.x;
const endX = linkWithState.link.range.end.y > y ? this._bufferService.cols : linkWithState.link.range.end.x;
for (let x = startX; x <= endX; x++) {
if (occupiedCells.has(x)) {
providerReply.splice(i--, 1);
break;
}
occupiedCells.add(x);
}
}
}
}
private _checkLinkProviderResult(index: number, position: IBufferCellPosition, linkProvided: boolean): boolean {
if (!this._activeProviderReplies) {
return linkProvided;
}
const links = this._activeProviderReplies.get(index);
// Check if every provider before this one has come back undefined
let hasLinkBefore = false;
for (let j = 0; j < index; j++) {
if (!this._activeProviderReplies.has(j) || this._activeProviderReplies.get(j)) {
hasLinkBefore = true;
}
}
// If all providers with higher priority came back undefined, then this provider's link for
// the position should be used
if (!hasLinkBefore && links) {
const linkAtPosition = links.find(link => this._linkAtPosition(link.link, position));
if (linkAtPosition) {
linkProvided = true;
this._handleNewLink(linkAtPosition);
}
}
// Check if all the providers have responded
if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) {
// Respect the order of the link providers
for (let j = 0; j < this._activeProviderReplies.size; j++) {
const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position));
if (currentLink) {
linkProvided = true;
this._handleNewLink(currentLink);
break;
}
}
}
return linkProvided;
}
private _onClick(event: MouseEvent): void {
if (!this._element || !this._mouseService || !this._currentLink) {
return;
}
const position = this._positionFromMouseEvent(event, this._element, this._mouseService);
if (!position) {
return;
}
if (this._linkAtPosition(this._currentLink.link, position)) {
this._currentLink.link.activate(event, this._currentLink.link.text);
}
}
private _clearCurrentLink(startRow?: number, endRow?: number): void {
if (!this._element || !this._currentLink || !this._lastMouseEvent) {
return;
}
// If we have a start and end row, check that the link is within it
if (!startRow || !endRow || (this._currentLink.link.range.start.y >= startRow && this._currentLink.link.range.end.y <= endRow)) {
this._linkLeave(this._element, this._currentLink.link, this._lastMouseEvent);
this._currentLink = undefined;
disposeArray(this._linkCacheDisposables);
}
}
private _handleNewLink(linkWithState: ILinkWithState): void {
if (!this._element || !this._lastMouseEvent || !this._mouseService) {
return;
}
const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService);
if (!position) {
return;
}
// Trigger hover if the we have a link at the position
if (this._linkAtPosition(linkWithState.link, position)) {
this._currentLink = linkWithState;
this._currentLink.state = {
decorations: {
underline: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.underline,
pointerCursor: linkWithState.link.decorations === undefined ? true : linkWithState.link.decorations.pointerCursor
},
isHovered: true
};
this._linkHover(this._element, linkWithState.link, this._lastMouseEvent);
// Add listener for tracking decorations changes
linkWithState.link.decorations = {} as ILinkDecorations;
Object.defineProperties(linkWithState.link.decorations, {
pointerCursor: {
get: () => this._currentLink?.state?.decorations.pointerCursor,
set: v => {
if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) {
this._currentLink.state.decorations.pointerCursor = v;
if (this._currentLink.state.isHovered) {
this._element?.classList.toggle('xterm-cursor-pointer', v);
}
}
}
},
underline: {
get: () => this._currentLink?.state?.decorations.underline,
set: v => {
if (this._currentLink?.state && this._currentLink?.state?.decorations.underline !== v) {
this._currentLink.state.decorations.underline = v;
if (this._currentLink.state.isHovered) {
this._fireUnderlineEvent(linkWithState.link, v);
}
}
}
}
});
// Add listener for rerendering
if (this._renderService) {
this._linkCacheDisposables.push(this._renderService.onRenderedBufferChange(e => {
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
// cleared.
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
this._clearCurrentLink(start, e.end + 1 + this._bufferService.buffer.ydisp);
}));
}
}
}
protected _linkHover(element: HTMLElement, link: ILink, event: MouseEvent): void {
if (this._currentLink?.state) {
this._currentLink.state.isHovered = true;
if (this._currentLink.state.decorations.underline) {
this._fireUnderlineEvent(link, true);
}
if (this._currentLink.state.decorations.pointerCursor) {
element.classList.add('xterm-cursor-pointer');
}
}
if (link.hover) {
link.hover(event, link.text);
}
}
private _fireUnderlineEvent(link: ILink, showEvent: boolean): void {
const range = link.range;
const scrollOffset = this._bufferService.buffer.ydisp;
const event = this._createLinkUnderlineEvent(range.start.x - 1, range.start.y - scrollOffset - 1, range.end.x, range.end.y - scrollOffset - 1, undefined);
const emitter = showEvent ? this._onShowLinkUnderline : this._onHideLinkUnderline;
emitter.fire(event);
}
protected _linkLeave(element: HTMLElement, link: ILink, event: MouseEvent): void {
if (this._currentLink?.state) {
this._currentLink.state.isHovered = false;
if (this._currentLink.state.decorations.underline) {
this._fireUnderlineEvent(link, false);
}
if (this._currentLink.state.decorations.pointerCursor) {
element.classList.remove('xterm-cursor-pointer');
}
}
if (link.leave) {
link.leave(event, link.text);
}
}
/**
* Check if the buffer position is within the link
* @param link
* @param position
*/
private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean {
const sameLine = link.range.start.y === link.range.end.y;
const wrappedFromLeft = link.range.start.y < position.y;
const wrappedToRight = link.range.end.y > position.y;
// If the start and end have the same y, then the position must be between start and end x
// If not, then handle each case seperately, depending on which way it wraps
return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) ||
(wrappedFromLeft && link.range.end.x >= position.x) ||
(wrappedToRight && link.range.start.x <= position.x) ||
(wrappedFromLeft && wrappedToRight)) &&
link.range.start.y <= position.y &&
link.range.end.y >= position.y;
}
/**
* Get the buffer position from a mouse event
* @param event
*/
private _positionFromMouseEvent(event: MouseEvent, element: HTMLElement, mouseService: IMouseService): IBufferCellPosition | undefined {
const coords = mouseService.getCoords(event, element, this._bufferService.cols, this._bufferService.rows);
if (!coords) {
return;
}
return { x: coords[0], y: coords[1] + this._bufferService.buffer.ydisp };
}
private _createLinkUnderlineEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent {
return { x1, y1, x2, y2, cols: this._bufferService.cols, fg };
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
// eslint-disable-next-line prefer-const
export let promptLabel = 'Terminal input';
// eslint-disable-next-line prefer-const
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
+238
View File
@@ -0,0 +1,238 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IMouseService, ISelectionService } from 'browser/services/Services';
import { IMouseZoneManager, IMouseZone } from 'browser/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
/**
* The MouseZoneManager allows components to register zones within the terminal
* that trigger hover and click callbacks.
*
* This class was intentionally made not so robust initially as the only case it
* needed to support was single-line links which never overlap. Improvements can
* be made in the future.
*/
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _zones: IMouseZone[] = [];
private _areZonesActive: boolean = false;
private _mouseMoveListener: (e: MouseEvent) => any;
private _mouseLeaveListener: (e: MouseEvent) => any;
private _clickListener: (e: MouseEvent) => any;
private _tooltipTimeout: number | undefined;
private _currentZone: IMouseZone | undefined;
private _lastHoverCoords: [number | undefined, number | undefined] = [undefined, undefined];
private _initialSelectionLength: number = 0;
constructor(
private readonly _element: HTMLElement,
private readonly _screenElement: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IMouseService private readonly _mouseService: IMouseService,
@ISelectionService private readonly _selectionService: ISelectionService,
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e)));
// These events are expensive, only listen to it when mouse zones are active
this._mouseMoveListener = e => this._onMouseMove(e);
this._mouseLeaveListener = e => this._onMouseLeave(e);
this._clickListener = e => this._onClick(e);
}
public dispose(): void {
super.dispose();
this._deactivate();
}
public add(zone: IMouseZone): void {
this._zones.push(zone);
if (this._zones.length === 1) {
this._activate();
}
}
public clearAll(start?: number, end?: number): void {
// Exit if there's nothing to clear
if (this._zones.length === 0) {
return;
}
// Clear all if start/end weren't set
if (!start || !end) {
start = 0;
end = this._bufferService.rows - 1;
}
// Iterate through zones and clear them out if they're within the range
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if ((zone.y1 > start && zone.y1 <= end + 1) ||
(zone.y2 > start && zone.y2 <= end + 1) ||
(zone.y1 < start && zone.y2 > end + 1)) {
if (this._currentZone && this._currentZone === zone) {
this._currentZone.leaveCallback();
this._currentZone = undefined;
}
this._zones.splice(i--, 1);
}
}
// Deactivate the mouse zone manager if all the zones have been removed
if (this._zones.length === 0) {
this._deactivate();
}
}
private _activate(): void {
if (!this._areZonesActive) {
this._areZonesActive = true;
this._element.addEventListener('mousemove', this._mouseMoveListener);
this._element.addEventListener('mouseleave', this._mouseLeaveListener);
this._element.addEventListener('click', this._clickListener);
}
}
private _deactivate(): void {
if (this._areZonesActive) {
this._areZonesActive = false;
this._element.removeEventListener('mousemove', this._mouseMoveListener);
this._element.removeEventListener('mouseleave', this._mouseLeaveListener);
this._element.removeEventListener('click', this._clickListener);
}
}
private _onMouseMove(e: MouseEvent): void {
// TODO: Ideally this would only clear the hover state when the mouse moves
// outside of the mouse zone
if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) {
this._onHover(e);
// Record the current coordinates
this._lastHoverCoords = [e.pageX, e.pageY];
}
}
private _onHover(e: MouseEvent): void {
const zone = this._findZoneEventAt(e);
// Do nothing if the zone is the same
if (zone === this._currentZone) {
return;
}
// Fire the hover end callback and cancel any existing timer if a new zone
// is being hovered
if (this._currentZone) {
this._currentZone.leaveCallback();
this._currentZone = undefined;
if (this._tooltipTimeout) {
clearTimeout(this._tooltipTimeout);
}
}
// Exit if there is not zone
if (!zone) {
return;
}
this._currentZone = zone;
// Trigger the hover callback
if (zone.hoverCallback) {
zone.hoverCallback(e);
}
// Restart the tooltip timeout
this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.options.linkTooltipHoverDuration);
}
private _onTooltip(e: MouseEvent): void {
this._tooltipTimeout = undefined;
const zone = this._findZoneEventAt(e);
if (zone && zone.tooltipCallback) {
zone.tooltipCallback(e);
}
}
private _onMouseDown(e: MouseEvent): void {
// Store current terminal selection length, to check if we're performing
// a selection operation
this._initialSelectionLength = this._getSelectionLength();
// Ignore the event if there are no zones active
if (!this._areZonesActive) {
return;
}
// Find the active zone, prevent event propagation if found to prevent other
// components from handling the mouse event.
const zone = this._findZoneEventAt(e);
if (zone?.willLinkActivate(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
private _onMouseLeave(e: MouseEvent): void {
// Fire the hover end callback and cancel any existing timer if the mouse
// leaves the terminal element
if (this._currentZone) {
this._currentZone.leaveCallback();
this._currentZone = undefined;
if (this._tooltipTimeout) {
clearTimeout(this._tooltipTimeout);
}
}
}
private _onClick(e: MouseEvent): void {
// Find the active zone and click it if found and no selection was
// being performed
const zone = this._findZoneEventAt(e);
const currentSelectionLength = this._getSelectionLength();
if (zone && currentSelectionLength === this._initialSelectionLength) {
zone.clickCallback(e);
e.preventDefault();
e.stopImmediatePropagation();
}
}
private _getSelectionLength(): number {
const selectionText = this._selectionService.selectionText;
return selectionText ? selectionText.length : 0;
}
private _findZoneEventAt(e: MouseEvent): IMouseZone | undefined {
const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows);
if (!coords) {
return undefined;
}
const x = coords[0];
const y = coords[1];
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if (zone.y1 === zone.y2) {
// Single line link
if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
return zone;
}
} else {
// Multi-line link
if ((y === zone.y1 && x >= zone.x1) ||
(y === zone.y2 && x < zone.x2) ||
(y > zone.y1 && y < zone.y2)) {
return zone;
}
}
}
return undefined;
}
}
+63
View File
@@ -0,0 +1,63 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
/**
* Debounces calls to render terminal rows using animation frames.
*/
export class RenderDebouncer implements IDisposable {
private _rowStart: number | undefined;
private _rowEnd: number | undefined;
private _rowCount: number | undefined;
private _animationFrame: number | undefined;
constructor(
private _renderCallback: (start: number, end: number) => void
) {
}
public dispose(): void {
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
this._rowCount = rowCount;
// Get the min/max row start/end for the arg values
rowStart = rowStart !== undefined ? rowStart : 0;
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
// Set the properties to the updated values
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
if (this._animationFrame) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => this._innerRefresh());
}
private _innerRefresh(): void {
// Make sure values are set
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
return;
}
// Clamp values
const start = Math.max(this._rowStart, 0);
const end = Math.min(this._rowEnd, this._rowCount - 1);
// Reset debouncer (this happens before render callback as the render could trigger it again)
this._rowStart = undefined;
this._rowEnd = undefined;
this._animationFrame = undefined;
// Run render callback
this._renderCallback(start, end);
}
}
+69
View File
@@ -0,0 +1,69 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
/**
* The screen device pixel ratio monitor allows listening for when the
* window.devicePixelRatio value changes. This is done not with polling but with
* the use of window.matchMedia to watch media queries. When the event fires,
* the listener will be reattached using a different media query to ensure that
* any further changes will register.
*
* The listener should fire on both window zoom changes and switching to a
* monitor with a different DPI.
*/
export class ScreenDprMonitor extends Disposable {
private _currentDevicePixelRatio: number = window.devicePixelRatio;
private _outerListener: ((this: MediaQueryList, ev: MediaQueryListEvent) => any) | undefined;
private _listener: ScreenDprListener | undefined;
private _resolutionMediaMatchList: MediaQueryList | undefined;
public setListener(listener: ScreenDprListener): void {
if (this._listener) {
this.clearListener();
}
this._listener = listener;
this._outerListener = () => {
if (!this._listener) {
return;
}
this._listener(window.devicePixelRatio, this._currentDevicePixelRatio);
this._updateDpr();
};
this._updateDpr();
}
public dispose(): void {
super.dispose();
this.clearListener();
}
private _updateDpr(): void {
if (!this._outerListener) {
return;
}
// Clear listeners for old DPR
this._resolutionMediaMatchList?.removeListener(this._outerListener);
// Add listeners for new DPR
this._currentDevicePixelRatio = window.devicePixelRatio;
this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`);
this._resolutionMediaMatchList.addListener(this._outerListener);
}
public clearListener(): void {
if (!this._resolutionMediaMatchList || !this._listener || !this._outerListener) {
return;
}
this._resolutionMediaMatchList.removeListener(this._outerListener);
this._resolutionMediaMatchList = undefined;
this._listener = undefined;
this._outerListener = undefined;
}
}
+1268
View File
File diff suppressed because it is too large Load Diff
+294
View File
@@ -0,0 +1,294 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IEvent } from 'common/EventEmitter';
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
export interface ITerminal extends IPublicTerminal, ICoreTerminal {
element: HTMLElement | undefined;
screenElement: HTMLElement | undefined;
browser: IBrowser;
buffer: IBuffer;
buffers: IBufferSet;
viewport: IViewport | undefined;
// TODO: We should remove options once components adopt optionsService
options: ITerminalOptions;
linkifier: ILinkifier;
linkifier2: ILinkifier2;
onBlur: IEvent<void>;
onFocus: IEvent<void>;
onA11yChar: IEvent<string>;
onA11yTab: IEvent<number>;
cancel(ev: Event, force?: boolean): boolean | void;
}
// Portions of the public API that are required by the internal Terminal
export interface IPublicTerminal extends IDisposable {
textarea: HTMLTextAreaElement | undefined;
rows: number;
cols: number;
buffer: IBuffer;
markers: IMarker[];
onCursorMove: IEvent<void>;
onData: IEvent<string>;
onBinary: IEvent<string>;
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
onLineFeed: IEvent<void>;
onScroll: IEvent<number>;
onSelectionChange: IEvent<void>;
onRender: IEvent<{ start: number, end: number }>;
onResize: IEvent<{ cols: number, rows: number }>;
onTitleChange: IEvent<string>;
blur(): void;
focus(): void;
resize(columns: number, rows: number): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): void;
addMarker(cursorYOffset: number): IMarker | undefined;
hasSelection(): boolean;
getSelection(): string;
getSelectionPosition(): ISelectionPosition | undefined;
clearSelection(): void;
select(column: number, row: number, length: number): void;
selectAll(): void;
selectLines(start: number, end: number): void;
dispose(): void;
scrollLines(amount: number): void;
scrollPages(pageCount: number): void;
scrollToTop(): void;
scrollToBottom(): void;
scrollToLine(line: number): void;
clear(): void;
write(data: string | Uint8Array, callback?: () => void): void;
paste(data: string): void;
refresh(start: number, end: number): void;
reset(): void;
}
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type LineData = CharData[];
export interface ICompositionHelper {
readonly isComposing: boolean;
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isWindows: boolean;
}
export interface IColorManager {
colors: IColorSet;
onOptionsChange(key: string): void;
}
export interface IColor {
css: string;
rgba: number; // 32-bit int with rgba in each byte
}
export interface IColorSet {
foreground: IColor;
background: IColor;
cursor: IColor;
cursorAccent: IColor;
selectionTransparent: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
ansi: IColor[];
contrastCache: IColorContrastCache;
}
export interface IColorContrastCache {
clear(): void;
setCss(bg: number, fg: number, value: string | null): void;
getCss(bg: number, fg: number): string | null | undefined;
setColor(bg: number, fg: number, value: IColor | null): void;
getColor(bg: number, fg: number): IColor | null | undefined;
}
export interface IPartialColorSet {
foreground: IColor;
background: IColor;
cursor?: IColor;
cursorAccent?: IColor;
selection?: IColor;
ansi: IColor[];
}
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): boolean;
onThemeChange(colors: IColorSet): void;
}
export interface IViewportRange {
start: IViewportRangePosition;
end: IViewportRangePosition;
}
export interface IViewportRangePosition {
x: number;
y: number;
}
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHoverTooltipCallback;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IRegisteredLinkMatcher extends ILinkMatcher {
priority: number;
}
export interface ILinkifierEvent {
x1: number;
y1: number;
x2: number;
y2: number;
cols: number;
fg: number | undefined;
}
export interface ILinkifier {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
onLinkTooltip: IEvent<ILinkifierEvent>;
attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ILinkifier2 {
onShowLinkUnderline: IEvent<ILinkifierEvent>;
onHideLinkUnderline: IEvent<ILinkifierEvent>;
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
}
export interface ILinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHoverTooltipCallback;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
export interface IMouseZone {
x1: number;
x2: number;
y1: number;
y2: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback: (e: MouseEvent) => any | undefined;
tooltipCallback: (e: MouseEvent) => any | undefined;
leaveCallback: () => any | undefined;
willLinkActivate: (e: MouseEvent) => boolean;
}
interface ILinkProvider {
provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void;
}
interface ILink {
range: IBufferRange;
text: string;
decorations?: ILinkDecorations;
activate(event: MouseEvent, text: string): void;
hover?(event: MouseEvent, text: string): void;
leave?(event: MouseEvent, text: string): void;
dispose?(): void;
}
interface ILinkDecorations {
pointerCursor: boolean;
underline: boolean;
}
interface IBufferRange {
start: IBufferCellPosition;
end: IBufferCellPosition;
}
interface IBufferCellPosition {
x: number;
y: number;
}
+267
View File
@@ -0,0 +1,267 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, IRenderService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number = 0;
private _lastScrollTop: number = 0;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
private _wheelPartialScroll: number = 0;
private _refreshAnimationFrame: number | null = null;
private _ignoreNextScrollEvent: boolean = false;
constructor(
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService
) {
super();
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChange(colors: IColorSet): void {
this._viewportElement.style.backgroundColor = colors.background.css;
}
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
private _refresh(immediate: boolean): void {
if (immediate) {
this._innerRefresh();
if (this._refreshAnimationFrame !== null) {
cancelAnimationFrame(this._refreshAnimationFrame);
}
return;
}
if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
}
}
private _innerRefresh(): void {
if (this._charSizeService.height > 0) {
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
this._lastRecordedBufferHeight = newBufferHeight;
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
}
}
// Sync scrollTop
const scrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
// want this event to scroll the terminal
this._ignoreNextScrollEvent = true;
this._viewportElement.scrollTop = scrollTop;
}
this._refreshAnimationFrame = null;
}
/**
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(immediate: boolean = false): void {
// If buffer height changed
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
this._refresh(immediate);
return;
}
// If viewport height changed
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
this._refresh(immediate);
return;
}
// If the buffer position doesn't match last scroll top
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) {
this._refresh(immediate);
return;
}
// If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
this._refresh(immediate);
return;
}
// If row height changed
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh(immediate);
return;
}
}
/**
* Handles scroll events on the viewport, calculating the new viewport and requesting the
* terminal to scroll to it.
* @param ev The scroll event.
*/
private _onScroll(ev: Event): void {
// Record current scroll top position
this._lastScrollTop = this._viewportElement.scrollTop;
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this._viewportElement.offsetParent) {
return;
}
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
if (this._ignoreNextScrollEvent) {
this._ignoreNextScrollEvent = false;
return;
}
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
const diff = newRow - this._bufferService.buffer.ydisp;
this._scrollLines(diff, true);
}
/**
* Handles bubbling of scroll event in case the viewport has reached top or bottom
* @param ev The scroll event.
* @param amount The amount scrolled
*/
private _bubbleScroll(ev: Event, amount: number): boolean {
const scrollPosFromTop = this._viewportElement.scrollTop + this._lastRecordedViewportHeight;
if ((amount < 0 && this._viewportElement.scrollTop !== 0) ||
(amount > 0 && scrollPosFromTop < this._lastRecordedBufferHeight)) {
if (ev.cancelable) {
ev.preventDefault();
}
return false;
}
return true;
}
/**
* Handles mouse wheel events by adjusting the viewport's scrollTop and delegating the actual
* scrolling to `onScroll`, this event needs to be attached manually by the consumer of
* `Viewport`.
* @param ev The mouse wheel event.
*/
public onWheel(ev: WheelEvent): boolean {
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return false;
}
this._viewportElement.scrollTop += amount;
return this._bubbleScroll(ev, amount);
}
private _getPixelsScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._currentRowHeight * this._bufferService.rows;
}
return amount;
}
/**
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
* is being used.
* @param ev The mouse wheel event.
*/
public getLinesScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_LINE
let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._bufferService.rows;
}
return amount;
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
const modifier = this._optionsService.options.fastScrollModifier;
// Multiply the scroll speed when the modifier is down
if ((modifier === 'alt' && ev.altKey) ||
(modifier === 'ctrl' && ev.ctrlKey) ||
(modifier === 'shift' && ev.shiftKey)) {
return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
}
return amount * this._optionsService.options.scrollSensitivity;
}
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
public onTouchStart(ev: TouchEvent): void {
this._lastTouchY = ev.touches[0].pageY;
}
/**
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): boolean {
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return false;
}
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
}
+234
View File
@@ -0,0 +1,234 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharSizeService } from 'browser/services/Services';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
interface IPosition {
start: number;
end: number;
}
/**
* Encapsulates the logic for handling compositionstart, compositionupdate and compositionend
* events, displaying the in-progress composition to the UI and forwarding the final composition
* to the handler.
*/
export class CompositionHelper {
/**
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
* IME. This variable determines whether the compositionText should be displayed on the UI.
*/
private _isComposing: boolean;
public get isComposing(): boolean { return this._isComposing; }
/**
* The position within the input textarea's value of the current composition.
*/
private _compositionPosition: IPosition;
/**
* Whether a composition is in the process of being sent, setting this to false will cancel any
* in-progress composition.
*/
private _isSendingComposition: boolean;
/**
* Data already sent due to keydown event.
*/
private _dataAlreadySent: string;
constructor(
private readonly _textarea: HTMLTextAreaElement,
private readonly _compositionView: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@ICoreService private readonly _coreService: ICoreService
) {
this._isComposing = false;
this._isSendingComposition = false;
this._compositionPosition = { start: 0, end: 0 };
this._dataAlreadySent = '';
}
/**
* Handles the compositionstart event, activating the composition view.
*/
public compositionstart(): void {
this._isComposing = true;
this._compositionPosition.start = this._textarea.value.length;
this._compositionView.textContent = '';
this._dataAlreadySent = '';
this._compositionView.classList.add('active');
}
/**
* Handles the compositionupdate event, updating the composition view.
* @param ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
this._compositionView.textContent = ev.data;
this.updateCompositionElements();
setTimeout(() => {
this._compositionPosition.end = this._textarea.value.length;
}, 0);
}
/**
* Handles the compositionend event, hiding the composition view and sending the composition to
* the handler.
*/
public compositionend(): void {
this._finalizeComposition(true);
}
/**
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
* @param ev The keydown event.
* @return Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
if (this._isComposing || this._isSendingComposition) {
if (ev.keyCode === 229) {
// Continue composing if the keyCode is the "composition character"
return false;
}
if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
// Continue composing if the keyCode is a modifier key
return false;
}
// Finish composition immediately. This is mainly here for the case where enter is
// pressed and the handler needs to be triggered before the command is executed.
this._finalizeComposition(false);
}
if (ev.keyCode === 229) {
// If the "composition character" is used but gets to this point it means a non-composition
// character (eg. numbers and punctuation) was pressed when the IME was active.
this._handleAnyTextareaChanges();
return false;
}
return true;
}
/**
* Finalizes the composition, resuming regular input actions. This is called when a composition
* is ending.
* @param waitForPropagation Whether to wait for events to propagate before sending
* the input. This should be false if a non-composition keystroke is entered before the
* compositionend event is triggered, such as enter, so that the composition is sent before
* the command is executed.
*/
private _finalizeComposition(waitForPropagation: boolean): void {
this._compositionView.classList.remove('active');
this._isComposing = false;
if (!waitForPropagation) {
// Cancel any delayed composition send requests and send the input immediately.
this._isSendingComposition = false;
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
this._coreService.triggerDataEvent(input, true);
} else {
// Make a deep copy of the composition position here as a new compositionstart event may
// fire before the setTimeout executes.
const currentCompositionPosition = {
start: this._compositionPosition.start,
end: this._compositionPosition.end
};
// Since composition* events happen before the changes take place in the textarea on most
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
// complete. This ensures the correct character is retrieved.
// This solution was used because:
// - The compositionend event's data property is unreliable, at least on Chromium
// - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to
// the following character if the following input is a vowel.
this._isSendingComposition = true;
setTimeout(() => {
// Ensure that the input has not already been sent
if (this._isSendingComposition) {
this._isSendingComposition = false;
let input;
// Add length of data already sent due to keydown event,
// otherwise input characters can be duplicated. (Issue #3191)
currentCompositionPosition.start += this._dataAlreadySent.length;
if (this._isComposing) {
// Use the end position to get the string if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
// (eg. 2) after a composition character.
input = this._textarea.value.substring(currentCompositionPosition.start);
}
if (input.length > 0) {
this._coreService.triggerDataEvent(input, true);
}
}
}, 0);
}
}
/**
* Apply any changes made to the textarea after the current event chain is allowed to complete.
* This should be called when not currently composing but a keydown event with the "composition
* character" (229) is triggered, in order to allow non-composition text to be entered when an
* IME is active.
*/
private _handleAnyTextareaChanges(): void {
const oldValue = this._textarea.value;
setTimeout(() => {
// Ignore if a composition has started since the timeout
if (!this._isComposing) {
const newValue = this._textarea.value;
const diff = newValue.replace(oldValue, '');
if (diff.length > 0) {
this._dataAlreadySent = diff;
this._coreService.triggerDataEvent(diff, true);
}
}
}, 0);
}
/**
* Positions the composition view on top of the cursor and the textarea just below it (so the
* IME helper dialog is positioned correctly).
* @param dontRecurse Whether to use setTimeout to recursively trigger another update, this is
* necessary as the IME events across browsers are not consistently triggered.
*/
public updateCompositionElements(dontRecurse?: boolean): void {
if (!this._isComposing) {
return;
}
if (this._bufferService.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
const cursorTop = this._bufferService.buffer.y * cellHeight;
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.fontFamily = this._optionsService.options.fontFamily;
this._compositionView.style.fontSize = this._optionsService.options.fontSize + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
}
}
+58
View File
@@ -0,0 +1,58 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
const rect = element.getBoundingClientRect();
return [event.clientX - rect.left, event.clientY - rect.top];
}
/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param event The mouse event.
* @param element The terminal's container element.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
// Coordinates cannot be measured if there are no valid
if (!hasValidCharSize) {
return undefined;
}
const coords = getCoordsRelativeToElement(event, element);
if (!coords) {
return undefined;
}
coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth);
coords[1] = Math.ceil(coords[1] / actualCellHeight);
// Ensure coordinates are within the terminal viewport. Note that selections
// need an addition point of precision to cover the end point (as characters
// cover half of one char and half of the next).
coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0));
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
return coords;
}
/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
*/
export function getRawByteCoords(coords: [number, number] | undefined): { x: number, y: number } | undefined {
if (!coords) {
return undefined;
}
// xterm sends raw bytes and starts at 32 (SP) for each.
return { x: coords[0] + 32, y: coords[1] + 32 };
}
+249
View File
@@ -0,0 +1,249 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { C0 } from 'common/data/EscapeSequences';
import { IBufferService } from 'common/services/Services';
const enum Direction {
UP = 'A',
DOWN = 'B',
RIGHT = 'C',
LEFT = 'D'
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
export function moveToCellSequence(targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const startX = bufferService.buffer.x;
const startY = bufferService.buffer.y;
// The alt buffer should try to navigate between rows
if (!bufferService.buffer.hasScrollback) {
return resetStartingRow(startX, startY, targetX, targetY, bufferService, applicationCursor) +
moveToRequestedRow(startY, targetY, bufferService, applicationCursor) +
moveToRequestedCol(startX, startY, targetX, targetY, bufferService, applicationCursor);
}
// Only move horizontally for the normal buffer
let direction;
if (startY === targetY) {
direction = startX > targetX ? Direction.LEFT : Direction.RIGHT;
return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor));
}
direction = startY > targetY ? Direction.LEFT : Direction.RIGHT;
const rowDifference = Math.abs(startY - targetY);
const cellsToMove = colsFromRowEnd(startY > targetY ? targetX : startX, bufferService) +
(rowDifference - 1) * bufferService.cols + 1 /* wrap around 1 row */ +
colsFromRowBeginning(startY > targetY ? startX : targetX, bufferService);
return repeat(cellsToMove, sequence(direction, applicationCursor));
}
/**
* Find the number of cols from a row beginning to a col.
*/
function colsFromRowBeginning(currX: number, bufferService: IBufferService): number {
return currX - 1;
}
/**
* Find the number of cols from a col to row end.
*/
function colsFromRowEnd(currX: number, bufferService: IBufferService): number {
return bufferService.cols - currX;
}
/**
* If the initial position of the cursor is on a row that is wrapped, move the
* cursor up to the first row that is not wrapped to have accurate vertical
* positioning.
*/
function resetStartingRow(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length === 0) {
return '';
}
return repeat(bufferLine(
startX, startY, startX,
startY - wrappedRowsForRow(bufferService, startY), false, bufferService
).length, sequence(Direction.LEFT, applicationCursor));
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService);
return repeat(rowsToMove, sequence(verticalDirection(startY, targetY), applicationCursor));
}
/**
* Move to the requested col on the ending row
*/
function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
let startRow;
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
} else {
startRow = startY;
}
const endRow = targetY;
const direction = horizontalDirection(startX, startY, targetX, targetY, bufferService, applicationCursor);
return repeat(bufferLine(
startX, startRow, targetX, endRow,
direction === Direction.RIGHT, bufferService
).length, sequence(direction, applicationCursor));
}
/**
* Utility functions
*/
/**
* Calculates the number of wrapped rows between the unwrapped starting and
* ending rows. These rows need to ignored since the cursor skips over them.
*/
function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number {
let wrappedRows = 0;
const startRow = startY - wrappedRowsForRow(bufferService, startY);
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
const line = bufferService.buffer.lines.get(startRow + (direction * i));
if (line && line.isWrapped) {
wrappedRows++;
}
}
return wrappedRows;
}
/**
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
let rowCount = 0;
let line = bufferService.buffer.lines.get(currentRow);
let lineWraps = line && line.isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) {
rowCount++;
line = bufferService.buffer.lines.get(--currentRow);
lineWraps = line && line.isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
let startRow;
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
} else {
startRow = startY;
}
if ((startX < targetX &&
startRow <= targetY) || // down/right or same y/right
(startX >= targetX &&
startRow < targetY)) { // down/left or same y/left
return Direction.RIGHT;
}
return Direction.LEFT;
}
/**
* Determines if the up or down arrow is needed
*/
function verticalDirection(startY: number, targetY: number): Direction {
return startY > targetY ? Direction.UP : Direction.DOWN;
}
/**
* Constructs the string of chars in the buffer from a starting row and col
* to an ending row and col
* @param startCol The starting column position
* @param startRow The starting row position
* @param endCol The ending column position
* @param endRow The ending row position
* @param forward Direction to move
*/
function bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean,
bufferService: IBufferService
): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > bufferService.cols - 1) {
bufferStr += bufferService.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += bufferService.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = bufferService.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + bufferService.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
function sequence(direction: Direction, applicationCursor: boolean): string {
const mod = applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
/**
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminalAddon, IDisposable, Terminal } from 'xterm';
export interface ILoadedAddon {
instance: ITerminalAddon;
dispose: () => void;
isDisposed: boolean;
}
export class AddonManager implements IDisposable {
protected _addons: ILoadedAddon[] = [];
constructor() {
}
public dispose(): void {
for (let i = this._addons.length - 1; i >= 0; i--) {
this._addons[i].instance.dispose();
}
}
public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {
const loadedAddon: ILoadedAddon = {
instance,
dispose: instance.dispose,
isDisposed: false
};
this._addons.push(loadedAddon);
instance.dispose = () => this._wrappedAddonDispose(loadedAddon);
instance.activate(<any>terminal);
}
private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {
if (loadedAddon.isDisposed) {
// Do nothing if already disposed
return;
}
let index = -1;
for (let i = 0; i < this._addons.length; i++) {
if (this._addons[i] === loadedAddon) {
index = i;
break;
}
}
if (index === -1) {
throw new Error('Could not dispose an addon that has not been loaded');
}
loadedAddon.isDisposed = true;
loadedAddon.dispose.apply(loadedAddon.instance);
this._addons.splice(index, 1);
}
}
+339
View File
@@ -0,0 +1,339 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider, FontWeight } from 'xterm';
import { ITerminal } from 'browser/Types';
import { IBufferLine, ICellData } from 'common/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../LocalizableStrings';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { AddonManager } from './AddonManager';
import { IParams } from 'common/parser/Types';
import { BufferSet } from 'common/buffer/BufferSet';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
private _parser: IParser | undefined;
private _buffer: BufferNamespaceApi | undefined;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
this._addonManager = new AddonManager();
}
private _checkProposedApi(): void {
if (!this._core.optionsService.options.allowProposedApi) {
throw new Error('You must set the allowProposedApi option to true to use proposed API');
}
}
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
public get onData(): IEvent<string> { return this._core.onData; }
public get onBinary(): IEvent<string> { return this._core.onBinary; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get element(): HTMLElement | undefined { return this._core.element; }
public get parser(): IParser {
this._checkProposedApi();
if (!this._parser) {
this._parser = new ParserApi(this._core);
}
return this._parser;
}
public get unicode(): IUnicodeHandling {
this._checkProposedApi();
return new UnicodeApi(this._core);
}
public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get buffer(): IBufferNamespaceApi {
this._checkProposedApi();
if (!this._buffer) {
this._buffer = new BufferNamespaceApi(this._core);
}
return this._buffer;
}
public get markers(): ReadonlyArray<IMarker> {
this._checkProposedApi();
return this._core.markers;
}
public blur(): void {
this._core.blur();
}
public focus(): void {
this._core.focus();
}
public resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
}
public open(parent: HTMLElement): void {
this._core.open(parent);
}
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
this._checkProposedApi();
return this._core.registerLinkMatcher(regex, handler, options);
}
public deregisterLinkMatcher(matcherId: number): void {
this._checkProposedApi();
this._core.deregisterLinkMatcher(matcherId);
}
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
this._checkProposedApi();
return this._core.registerLinkProvider(linkProvider);
}
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
this._checkProposedApi();
return this._core.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): void {
this._checkProposedApi();
this._core.deregisterCharacterJoiner(joinerId);
}
public registerMarker(cursorYOffset: number): IMarker | undefined {
this._checkProposedApi();
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
}
public addMarker(cursorYOffset: number): IMarker | undefined {
return this.registerMarker(cursorYOffset);
}
public hasSelection(): boolean {
return this._core.hasSelection();
}
public select(column: number, row: number, length: number): void {
this._verifyIntegers(column, row, length);
this._core.select(column, row, length);
}
public getSelection(): string {
return this._core.getSelection();
}
public getSelectionPosition(): ISelectionPosition | undefined {
return this._core.getSelectionPosition();
}
public clearSelection(): void {
this._core.clearSelection();
}
public selectAll(): void {
this._core.selectAll();
}
public selectLines(start: number, end: number): void {
this._verifyIntegers(start, end);
this._core.selectLines(start, end);
}
public dispose(): void {
this._addonManager.dispose();
this._core.dispose();
}
public scrollLines(amount: number): void {
this._verifyIntegers(amount);
this._core.scrollLines(amount);
}
public scrollPages(pageCount: number): void {
this._verifyIntegers(pageCount);
this._core.scrollPages(pageCount);
}
public scrollToTop(): void {
this._core.scrollToTop();
}
public scrollToBottom(): void {
this._core.scrollToBottom();
}
public scrollToLine(line: number): void {
this._verifyIntegers(line);
this._core.scrollToLine(line);
}
public clear(): void {
this._core.clear();
}
public write(data: string | Uint8Array, callback?: () => void): void {
this._core.write(data, callback);
}
public writeUtf8(data: Uint8Array, callback?: () => void): void {
this._core.write(data, callback);
}
public writeln(data: string | Uint8Array, callback?: () => void): void {
this._core.write(data);
this._core.write('\r\n', callback);
}
public paste(data: string): void {
this._core.paste(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean;
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
public getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight;
public getOption(key: string): any;
public getOption(key: any): any {
return this._core.optionsService.getOption(key);
}
public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void;
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void;
public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void;
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void;
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
public setOption(key: 'theme', value: ITheme): void;
public setOption(key: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void;
public setOption(key: any, value: any): void {
this._core.optionsService.setOption(key, value);
}
public refresh(start: number, end: number): void {
this._verifyIntegers(start, end);
this._core.refresh(start, end);
}
public reset(): void {
this._core.reset();
}
public loadAddon(addon: ITerminalAddon): void {
return this._addonManager.loadAddon(this, addon);
}
public static get strings(): ILocalizableStrings {
return Strings;
}
private _verifyIntegers(...values: number[]): void {
for (const value of values) {
if (value === Infinity || isNaN(value) || value % 1 !== 0) {
throw new Error('This API only accepts integers');
}
}
}
}
class BufferApiView implements IBufferApi {
constructor(
private _buffer: IBuffer,
public readonly type: 'normal' | 'alternate'
) { }
public init(buffer: IBuffer): BufferApiView {
this._buffer = buffer;
return this;
}
public get cursorY(): number { return this._buffer.y; }
public get cursorX(): number { return this._buffer.x; }
public get viewportY(): number { return this._buffer.ydisp; }
public get baseY(): number { return this._buffer.ybase; }
public get length(): number { return this._buffer.lines.length; }
public getLine(y: number): IBufferLineApi | undefined {
const line = this._buffer.lines.get(y);
if (!line) {
return undefined;
}
return new BufferLineApiView(line);
}
public getNullCell(): IBufferCellApi { return new CellData(); }
}
class BufferNamespaceApi implements IBufferNamespaceApi {
private _normal: BufferApiView;
private _alternate: BufferApiView;
private _onBufferChange = new EventEmitter<IBufferApi>();
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
constructor(private _core: ITerminal) {
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');
this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active));
}
public get active(): IBufferApi {
if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }
if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }
throw new Error('Active buffer is neither normal nor alternate');
}
public get normal(): IBufferApi {
return this._normal.init(this._core.buffers.normal);
}
public get alternate(): IBufferApi {
return this._alternate.init(this._core.buffers.alt);
}
}
class BufferLineApiView implements IBufferLineApi {
constructor(private _line: IBufferLine) { }
public get isWrapped(): boolean { return this._line.isWrapped; }
public get length(): number { return this._line.length; }
public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {
if (x < 0 || x >= this._line.length) {
return undefined;
}
if (cell) {
this._line.loadCell(x, <ICellData>cell);
return cell;
}
return this._line.loadCell(x, new CellData());
}
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
return this._line.translateToString(trimRight, startColumn, endColumn);
}
}
class ParserApi implements IParser {
constructor(private _core: ITerminal) { }
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this.registerCsiHandler(id, callback);
}
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this.registerDcsHandler(id, callback);
}
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this._core.addEscHandler(id, handler);
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this.registerEscHandler(id, handler);
}
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this.registerOscHandler(ident, callback);
}
}
class UnicodeApi implements IUnicodeHandling {
constructor(private _core: ITerminal) { }
public register(provider: IUnicodeVersionProvider): void {
this._core.unicodeService.register(provider);
}
public get versions(): string[] {
return this._core.unicodeService.versions;
}
public get activeVersion(): string {
return this._core.unicodeService.activeVersion;
}
public set activeVersion(version: string) {
this._core.unicodeService.activeVersion = version;
}
}
+477
View File
@@ -0,0 +1,477 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
import { ICellData } from 'common/Types';
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData';
import { IColorSet, IColor } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { channels, color, rgba } from 'browser/Color';
import { removeElementFromParent } from 'browser/Dom';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
private _scaledCharHeight: number = 0;
private _scaledCellWidth: number = 0;
private _scaledCellHeight: number = 0;
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
protected _charAtlas: BaseCharAtlas | undefined;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor(
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet,
private _rendererId: number,
protected readonly _bufferService: IBufferService,
protected readonly _optionsService: IOptionsService
) {
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
}
public dispose(): void {
removeElementFromParent(this._canvas);
this._charAtlas?.dispose();
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
}
public onOptionsChanged(): void {}
public onBlur(): void {}
public onFocus(): void {}
public onCursorMove(): void {}
public onGridChanged(startRow: number, endRow: number): void {}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
public setColors(colorSet: IColorSet): void {
this._refreshCharAtlas(colorSet);
}
protected _setTransparency(alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(this._colors);
this.onGridChanged(0, this._bufferService.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(colorSet: IColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}
public resize(dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledCharLeft = dim.scaledCharLeft;
this._scaledCharTop = dim.scaledCharTop;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
this._refreshCharAtlas(this._colors);
}
public abstract reset(): void;
/**
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
* @param x The column to start at.
* @param y The row to start at
* @param width The number of columns to fill.
* @param height The number of rows to fill.
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
width * this._scaledCellWidth,
window.devicePixelRatio);
}
/**
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
window.devicePixelRatio * width,
this._scaledCellHeight);
}
/**
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
* strokeStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
this._ctx.lineWidth = window.devicePixelRatio;
this._ctx.strokeRect(
x * this._scaledCellWidth + window.devicePixelRatio / 2,
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
width * this._scaledCellWidth - window.devicePixelRatio,
(height * this._scaledCellHeight) - window.devicePixelRatio);
}
/**
* Clears the entire canvas.
*/
protected _clearAll(): void {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
/**
* Clears 1+ cells completely.
* @param x The column to start at.
* @param y The row to start at.
* @param width The number of columns to clear.
* @param height The number of rows to clear.
*/
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
}
}
/**
* Draws a truecolor character at the cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character. This uses the existing fillStyle on the context.
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
* @param color The color of the character.
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = 'middle';
this._clipRow(y);
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
}
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
* @param chars The character or characters.
* @param code The character code.
* @param width The width of the characters.
* @param x The column to draw at.
* @param y The row to draw at.
* @param fg The foreground color, in the format stored within the attributes.
* @param bg The background color, in the format stored within the attributes.
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const contrastColor = this._getContrastColor(cell);
// skip cache right away if we draw in RGB
// Note: to avoid bad runtime JoinedCellData will be skipped
// in the cache handler itself (atlasDidDraw == false) and
// fall through to uncached later down below
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
this._drawUncachedChars(cell, x, y, contrastColor);
return;
}
let fg;
let bg;
if (cell.isInverse()) {
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
} else {
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
}
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
this._currentGlyphIdentifier.bg = bg;
this._currentGlyphIdentifier.fg = fg;
this._currentGlyphIdentifier.bold = !!cell.isBold();
this._currentGlyphIdentifier.dim = !!cell.isDim();
this._currentGlyphIdentifier.italic = !!cell.isItalic();
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
this._currentGlyphIdentifier,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
if (!atlasDidDraw) {
this._drawUncachedChars(cell, x, y);
}
}
/**
* Draws one or more characters at one or more cells. The character(s) will be
* clipped to ensure that they fit with the cell(s), including the cell to the
* right if the last character is a wide character.
* @param chars The character.
* @param width The width of the character.
* @param fg The foreground color, in the format stored within the attributes.
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'middle';
if (cell.isInverse()) {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isBgDefault()) {
this._ctx.fillStyle = color.opaque(this._colors.background).css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
this._clipRow(y);
// Apply alpha to dim the character
if (cell.isDim()) {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
this._ctx.restore();
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
*/
private _clipRow(y: number): void {
this._ctx.beginPath();
this._ctx.rect(
0,
y * this._scaledCellHeight,
this._bufferService.cols * this._scaledCellWidth,
this._scaledCellHeight);
this._ctx.clip();
}
/**
* Gets the current font.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
}
private _getContrastColor(cell: CellData): IColor | undefined {
if (this._optionsService.options.minimumContrastRatio === 1) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
let fgColor = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bgColor = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
const isBold = !!cell.isInverse();
if (isInverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
if (!result) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
return undefined;
}
const color: IColor = {
css: channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
),
rgba: result
};
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
return color;
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._colors.ansi[bgColor].rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.foreground.rgba;
}
return this._colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._colors.ansi[fgColor].rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.background.rgba;
}
return this._colors.foreground.rgba;
}
}
}
@@ -0,0 +1,326 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IBufferLine, ICellData, CharData } from 'common/Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from 'browser/renderer/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IBufferService } from 'common/services/Services';
export class JoinedCellData extends AttributeData implements ICellData {
private _width: number;
// .content carries no meaning for joined CellData, simply nullify it
// thus we have to overload all other .content accessors
public content: number = 0;
public fg: number;
public bg: number;
public combinedData: string = '';
constructor(firstCell: ICellData, chars: string, width: number) {
super();
this.fg = firstCell.fg;
this.bg = firstCell.bg;
this.combinedData = chars;
this._width = width;
}
public isCombined(): number {
// always mark joined cell data as combined
return Content.IS_COMBINED_MASK;
}
public getWidth(): number {
return this._width;
}
public getChars(): string {
return this.combinedData;
}
public getCode(): number {
// code always gets the highest possible fake codepoint (read as -1)
// this is needed as code is used by caches as identifier
return 0x1FFFFF;
}
public setFromCharData(value: CharData): void {
throw new Error('not implemented');
}
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
private _characterJoiners: ICharacterJoiner[] = [];
private _nextCharacterJoinerId: number = 0;
private _workCell: CellData = new CellData();
constructor(private _bufferService: IBufferService) { }
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
const joiner: ICharacterJoiner = {
id: this._nextCharacterJoinerId++,
handler
};
this._characterJoiners.push(joiner);
return joiner.id;
}
public deregisterCharacterJoiner(joinerId: number): boolean {
for (let i = 0; i < this._characterJoiners.length; i++) {
if (this._characterJoiners[i].id === joinerId) {
this._characterJoiners.splice(i, 1);
return true;
}
}
return false;
}
public getJoinedCharacters(row: number): [number, number][] {
if (this._characterJoiners.length === 0) {
return [];
}
const line = this._bufferService.buffer.lines.get(row);
if (!line || line.length === 0) {
return [];
}
const ranges: [number, number][] = [];
const lineStr = line.translateToString(true);
// Because some cells can be represented by multiple javascript characters,
// we track the cell and the string indexes separately. This allows us to
// translate the string ranges we get from the joiners back into cell ranges
// for use when rendering
let rangeStartColumn = 0;
let currentStringIndex = 0;
let rangeStartStringIndex = 0;
let rangeAttrFG = line.getFg(0);
let rangeAttrBG = line.getBg(0);
for (let x = 0; x < line.getTrimmedLength(); x++) {
line.loadCell(x, this._workCell);
if (this._workCell.getWidth() === 0) {
// If this character is of width 0, skip it.
continue;
}
// End of range
if (this._workCell.fg !== rangeAttrFG || this._workCell.bg !== rangeAttrBG) {
// If we ended up with a sequence of more than one character,
// look for ranges to join.
if (x - rangeStartColumn > 1) {
const joinedRanges = this._getJoinedRanges(
lineStr,
rangeStartStringIndex,
currentStringIndex,
line,
rangeStartColumn
);
for (let i = 0; i < joinedRanges.length; i++) {
ranges.push(joinedRanges[i]);
}
}
// Reset our markers for a new range.
rangeStartColumn = x;
rangeStartStringIndex = currentStringIndex;
rangeAttrFG = this._workCell.fg;
rangeAttrBG = this._workCell.bg;
}
currentStringIndex += this._workCell.getChars().length || WHITESPACE_CELL_CHAR.length;
}
// Process any trailing ranges.
if (this._bufferService.cols - rangeStartColumn > 1) {
const joinedRanges = this._getJoinedRanges(
lineStr,
rangeStartStringIndex,
currentStringIndex,
line,
rangeStartColumn
);
for (let i = 0; i < joinedRanges.length; i++) {
ranges.push(joinedRanges[i]);
}
}
return ranges;
}
/**
* Given a segment of a line of text, find all ranges of text that should be
* joined in a single rendering unit. Ranges are internally converted to
* column ranges, rather than string ranges.
* @param line String representation of the full line of text
* @param startIndex Start position of the range to search in the string (inclusive)
* @param endIndex End position of the range to search in the string (exclusive)
*/
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
const text = line.substring(startIndex, endIndex);
// At this point we already know that there is at least one joiner so
// we can just pull its value and assign it directly rather than
// merging it into an empty array, which incurs unnecessary writes.
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
for (let i = 1; i < this._characterJoiners.length; i++) {
// We merge any overlapping ranges across the different joiners
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
}
}
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
return joinedRanges;
}
/**
* Modifies the provided ranges in-place to adjust for variations between
* string length and cell width so that the range represents a cell range,
* rather than the string range the joiner provides.
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
* @param line Cell data for the relevant line in the terminal
* @param startCol Offset within the line to start from
*/
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
let currentRangeIndex = 0;
let currentRangeStarted = false;
let currentStringIndex = 0;
let currentRange = ranges[currentRangeIndex];
// If we got through all of the ranges, stop searching
if (!currentRange) {
return;
}
for (let x = startCol; x < this._bufferService.cols; x++) {
const width = line.getWidth(x);
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
// We skip zero-width characters when creating the string to join the text
// so we do the same here
if (width === 0) {
continue;
}
// Adjust the start of the range
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
currentRange[0] = x;
currentRangeStarted = true;
}
// Adjust the end of the range
if (currentRange[1] <= currentStringIndex) {
currentRange[1] = x;
// We're finished with this range, so we move to the next one
currentRange = ranges[++currentRangeIndex];
// If there are no more ranges left, stop searching
if (!currentRange) {
break;
}
// Ranges can be on adjacent characters. Because the end index of the
// ranges are exclusive, this means that the index for the start of a
// range can be the same as the end index of the previous range. To
// account for the start of the next range, we check here just in case.
if (currentRange[0] <= currentStringIndex) {
currentRange[0] = x;
currentRangeStarted = true;
} else {
currentRangeStarted = false;
}
}
// Adjust the string index based on the character length to line up with
// the column adjustment
currentStringIndex += length;
}
// If there is still a range left at the end, it must extend all the way to
// the end of the line.
if (currentRange) {
currentRange[1] = this._bufferService.cols;
}
}
/**
* Merges the range defined by the provided start and end into the list of
* existing ranges. The merge is done in place on the existing range for
* performance and is also returned.
* @param ranges Existing range list
* @param newRange Tuple of two numbers representing the new range to merge in.
* @returns The ranges input with the new range merged in place
*/
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
let inRange = false;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (!inRange) {
if (newRange[1] <= range[0]) {
// Case 1: New range is before the search range
ranges.splice(i, 0, newRange);
return ranges;
}
if (newRange[1] <= range[1]) {
// Case 2: New range is either wholly contained within the
// search range or overlaps with the front of it
range[0] = Math.min(newRange[0], range[0]);
return ranges;
}
if (newRange[0] < range[1]) {
// Case 3: New range either wholly contains the search range
// or overlaps with the end of it
range[0] = Math.min(newRange[0], range[0]);
inRange = true;
}
// Case 4: New range starts after the search range
continue;
} else {
if (newRange[1] <= range[0]) {
// Case 5: New range extends from previous range but doesn't
// reach the current one
ranges[i - 1][1] = newRange[1];
return ranges;
}
if (newRange[1] <= range[1]) {
// Case 6: New range extends from prvious range into the
// current range
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
ranges.splice(i, 1);
return ranges;
}
// Case 7: New range extends from previous range past the
// end of the current range
ranges.splice(i, 1);
i--;
}
}
if (inRange) {
// Case 8: New range extends past the last existing range
ranges[ranges.length - 1][1] = newRange[1];
} else {
// Case 9: New range starts after the last existing range
ranges.push(newRange);
}
return ranges;
}
}
+374
View File
@@ -0,0 +1,374 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
style: string;
width: number;
}
/**
* The time between cursor blinks.
*/
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
public reset(): void {
this._clearCursor();
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = undefined;
this.onOptionsChanged();
}
}
public onBlur(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume();
} else {
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
}
public onOptionsChanged(): void {
if (this._optionsService.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
});
}
} else {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onCursorMove(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.restartBlinkAnimation();
}
}
public onGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation();
}
}
private _render(triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor();
return;
}
const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
// Don't draw the cursor if it's off-screen
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
this._clearCursor();
return;
}
// in case cursor.x == cols adjust visual cursor to cols - 1
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
this._bufferService.buffer.lines.get(cursorY)!.loadCell(cursorX, this._cell);
if (this._cell.content === undefined) {
return;
}
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
const cursorStyle = this._optionsService.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
}
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = cursorStyle;
this._state.width = this._cell.getWidth();
return;
}
// Don't draw the cursor if it's blinking
if (this._cursorBlinkStateManager && !this._cursorBlinkStateManager.isCursorVisible) {
this._clearCursor();
return;
}
if (this._state) {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === this._optionsService.options.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
}
this._clearCursor();
}
this._ctx.save();
this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = this._optionsService.options.cursorStyle;
this._state.width = this._cell.getWidth();
}
private _clearCursor(): void {
if (this._state) {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
this._state = {
x: 0,
y: 0,
isFocused: false,
style: '',
width: 0
};
}
}
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._colors.cursorAccent.css;
this._fillCharTrueColor(cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
}
class CursorBlinkStateManager {
public isCursorVisible: boolean;
private _animationFrame: number | undefined;
private _blinkStartTimeout: number | undefined;
private _blinkInterval: number | undefined;
/**
* The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers
* multiple times over a short period.
*/
private _animationTimeRestarted: number | undefined;
constructor(
isFocused: boolean,
private _renderCallback: () => void
) {
this.isCursorVisible = true;
if (isFocused) {
this._restartInterval();
}
}
public get isPaused(): boolean { return !(this._blinkStartTimeout || this._blinkInterval); }
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public restartBlinkAnimation(): void {
if (this.isPaused) {
return;
}
// Save a timestamp so that the restart can be done on the next interval
this._animationTimeRestarted = Date.now();
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}
}
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
}
// Setup the initial timeout which will hide the cursor, this is done before
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
if (time > 0) {
this._restartInterval(time);
return;
}
}
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
// Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = undefined;
this._restartInterval(time);
return;
}
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
}, BLINK_INTERVAL);
}, timeToStart);
}
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
public resume(): void {
// Clear out any existing timers just in case
this.pause();
this._animationTimeRestarted = undefined;
this._restartInterval();
this.restartBlinkAnimation();
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export class GridCache<T> {
public cache: (T | undefined)[][];
public constructor() {
this.cache = [];
}
public resize(width: number, height: number): void {
for (let x = 0; x < width; x++) {
if (this.cache.length <= x) {
this.cache.push([]);
}
for (let y = this.cache[x].length; y < height; y++) {
this.cache[x].push(undefined);
}
this.cache[x].length = height;
}
this.cache.length = width;
}
public clear(): void {
for (let x = 0; x < this.cache.length; x++) {
for (let y = 0; y < this.cache[x].length; y++) {
this.cache[x][y] = undefined;
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
bufferService: IBufferService,
optionsService: IOptionsService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = undefined;
}
public reset(): void {
this._clearCurrentLink();
}
private _clearCurrentLink(): void {
if (this._state) {
this._clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
const middleRowCount = this._state.y2 - this._state.y1 - 1;
if (middleRowCount > 0) {
this._clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
}
this._clearCells(0, this._state.y2, this._state.x2, 1);
this._state = undefined;
}
}
private _onShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (e.fg && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
if (e.y1 === e.y2) {
// Single line link
this._fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
} else {
// Multi-line link
this._fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
for (let y = e.y1 + 1; y < e.y2; y++) {
this._fillBottomLineAtCells(0, y, e.cols);
}
this._fillBottomLineAtCells(0, e.y2, e.x2);
}
this._state = e;
}
private _onHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
+221
View File
@@ -0,0 +1,221 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRedrawEvent } from 'browser/renderer/Types';
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
let nextRendererId = 1;
export class Renderer extends Disposable implements IRenderer {
private _id = nextRendererId++;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
private _characterJoinerRegistry: ICharacterJoinerRegistry;
public dimensions: IRenderDimensions;
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
constructor(
private _colors: IColorSet,
private readonly _screenElement: HTMLElement,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IBufferService private readonly _bufferService: IBufferService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreService coreService: ICoreService,
@ICoreBrowserService coreBrowserService: ICoreBrowserService
) {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
this._renderLayers = [
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService),
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, _optionsService, coreService, coreBrowserService)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this.onOptionsChanged();
}
public dispose(): void {
for (const l of this._renderLayers) {
l.dispose();
}
super.dispose();
removeTerminalFromCache(this._id);
}
public onDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio;
this.onResize(this._bufferService.cols, this._bufferService.rows);
}
}
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
for (const l of this._renderLayers) {
l.setColors(this._colors);
l.reset();
}
}
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
// Resize the screen
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
}
public onCharSizeChanged(): void {
this.onResize(this._bufferService.cols, this._bufferService.rows);
}
public onBlur(): void {
this._runOperation(l => l.onBlur());
}
public onFocus(): void {
this._runOperation(l => l.onFocus());
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
}
public onCursorMove(): void {
this._runOperation(l => l.onCursorMove());
}
public onOptionsChanged(): void {
this._runOperation(l => l.onOptionsChanged());
}
public clear(): void {
this._runOperation(l => l.reset());
}
private _runOperation(operation: (layer: IRenderLayer) => void): void {
for (const l of this._renderLayers) {
operation(l);
}
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
public renderRows(start: number, end: number): void {
for (const l of this._renderLayers) {
l.onGridChanged(start, end);
}
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
if (!this._charSizeService.hasValidSize) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * window.devicePixelRatio);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. ICharSizeService.width/height by itself
// is insufficient when the page is not at 100% zoom level as it's measured
// in CSS pixels, but the actual char size on the canvas can differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
}
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
}
public deregisterCharacterJoiner(joinerId: number): boolean {
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
}
}
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
throw new Error('value must not be falsy');
}
return value;
}
+128
View File
@@ -0,0 +1,128 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IBufferService, IOptionsService } from 'common/services/Services';
interface ISelectionState {
start?: [number, number];
end?: [number, number];
columnSelectMode?: boolean;
ydisp?: number;
}
export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
this._clearState();
}
private _clearState(): void {
this._state = {
start: undefined,
end: undefined,
columnSelectMode: undefined,
ydisp: undefined
};
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._clearState();
}
public reset(): void {
if (this._state.start && this._state.end) {
this._clearState();
this._clearAll();
}
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Selection has not changed
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
return;
}
// Remove all selections
this._clearAll();
// Selection does not exist
if (!start || !end) {
this._clearState();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
this._state.ydisp = this._bufferService.buffer.ydisp;
return;
}
this._ctx.fillStyle = this._colors.selectionTransparent.css;
if (columnSelectMode) {
const startCol = start[0];
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this._fillCells(startCol, viewportCappedStartRow, width, height);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportEndRow ? end[0] : this._bufferService.cols;
this._fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this._fillCells(0, viewportCappedStartRow + 1, this._bufferService.cols, middleRowsCount);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
this._fillCells(0, viewportCappedEndRow, endCol, 1);
}
}
// Save state for next render
this._state.start = [start[0], start[1]];
this._state.end = [end[0], end[1]];
this._state.columnSelectMode = columnSelectMode;
this._state.ydisp = this._bufferService.buffer.ydisp;
}
private _didStateChange(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean, ydisp: number): boolean {
return !this._areCoordinatesEqual(start, this._state.start) ||
!this._areCoordinatesEqual(end, this._state.end) ||
columnSelectMode !== this._state.columnSelectMode ||
ydisp !== this._state.ydisp;
}
private _areCoordinatesEqual(coord1: [number, number] | undefined, coord2: [number, number] | undefined): boolean {
if (!coord1 || !coord2) {
return false;
}
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
}
}
+328
View File
@@ -0,0 +1,328 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from 'browser/renderer/GridCache';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
import { IColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService } from 'common/services/Services';
/**
* This CharData looks like a null character, which will forc a clear and render
* when the character changes (a regular space ' ' character may not as it's
* drawn state is a cleared cell).
*/
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
export class TextRenderLayer extends BaseRenderLayer {
private _state: GridCache<CharData>;
private _characterWidth: number = 0;
private _characterFont: string = '';
private _characterOverlapCache: { [key: string]: boolean } = {};
private _characterJoinerRegistry: ICharacterJoinerRegistry;
private _workCell = new CellData();
constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
characterJoinerRegistry: ICharacterJoinerRegistry,
alpha: boolean,
rendererId: number,
bufferService: IBufferService,
optionsService: IOptionsService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
this._state = new GridCache<CharData>();
this._characterJoinerRegistry = characterJoinerRegistry;
}
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
// Resizing the canvas discards the contents of the canvas so clear state
this._state.clear();
this._state.resize(this._bufferService.cols, this._bufferService.rows);
}
public reset(): void {
this._state.clear();
this._clearAll();
}
private _forEachCell(
firstRow: number,
lastRow: number,
joinerRegistry: ICharacterJoinerRegistry | null,
callback: (
cell: ICellData,
x: number,
y: number
) => void
): void {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + this._bufferService.buffer.ydisp;
const line = this._bufferService.buffer.lines.get(row);
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
for (let x = 0; x < this._bufferService.cols; x++) {
line!.loadCell(x, this._workCell);
let cell = this._workCell;
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (cell.getWidth() === 0) {
continue;
}
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
cell = new JoinedCellData(
this._workCell,
line!.translateToString(true, range[0], range[1]),
range[1] - range[0]
);
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
}
// If the character is an overlapping char and the character to the
// right is a space, take ownership of the cell to the right. We skip
// this check for joined characters because their rendering likely won't
// yield the same result as rendering the last character individually.
if (!isJoined && this._isOverlapping(cell)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
// space is added. Without this, the first half of `b` would never
// get removed, and `a` would not re-render because it thinks it's
// already in the correct state.
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
if (lastCharX < line!.length - 1 && line!.getCodePoint(lastCharX + 1) === NULL_CELL_CODE) {
// patch width to 2
cell.content &= ~Content.WIDTH_MASK;
cell.content |= 2 << Content.WIDTH_SHIFT;
// this._clearChar(x + 1, y);
// The overlapping char's char data will force a clear and render when the
// overlapping char is no longer to the left of the character and also when
// the space changes to another character.
// this._state.cache[x + 1][y] = OVERLAP_OWNED_CHAR_DATA;
}
}
callback(
cell,
x,
y
);
x = lastCharX;
}
}
}
/**
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
* same color together to reduce draw calls.
*/
private _drawBackground(firstRow: number, lastRow: number): void {
const ctx = this._ctx;
const cols = this._bufferService.cols;
let startX: number = 0;
let startY: number = 0;
let prevFillStyle: string | null = null;
ctx.save();
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
if (cell.isInverse()) {
if (cell.isFgDefault()) {
nextFillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
}
} else if (cell.isBgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else if (cell.isBgPalette()) {
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
}
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
// don't need to draw anything.
startX = x;
startY = y;
}
if (y !== startY) {
// our row changed, draw the previous row
ctx.fillStyle = prevFillStyle || '';
this._fillCells(startX, startY, cols - startX, 1);
startX = x;
startY = y;
} else if (prevFillStyle !== nextFillStyle) {
// our color changed, draw the previous characters in this row
ctx.fillStyle = prevFillStyle || '';
this._fillCells(startX, startY, x - startX, 1);
startX = x;
startY = y;
}
prevFillStyle = nextFillStyle;
});
// flush the last color we encountered
if (prevFillStyle !== null) {
ctx.fillStyle = prevFillStyle;
this._fillCells(startX, startY, cols - startX, 1);
}
ctx.restore();
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
this._drawChars(cell, x, y);
if (cell.isUnderline()) {
this._ctx.save();
if (cell.isInverse()) {
if (cell.isBgDefault()) {
this._ctx.fillStyle = this._colors.background.css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
this._fillBottomLineAtCells(x, y, cell.getWidth());
this._ctx.restore();
}
});
}
public onGridChanged(firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
if (this._charAtlas) {
this._charAtlas.beginFrame();
}
this._clearCells(0, firstRow, this._bufferService.cols, lastRow - firstRow + 1);
this._drawBackground(firstRow, lastRow);
this._drawForeground(firstRow, lastRow);
}
public onOptionsChanged(): void {
this._setTransparency(this._optionsService.options.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(cell: ICellData): boolean {
// Only single cell characters can be overlapping, rendering issues can
// occur without this check
if (cell.getWidth() !== 1) {
return false;
}
// We assume that any ascii character will not overlap
if (cell.getCode() < 256) {
return false;
}
const chars = cell.getChars();
// Deliver from cache if available
if (this._characterOverlapCache.hasOwnProperty(chars)) {
return this._characterOverlapCache[chars];
}
// Setup the font
this._ctx.save();
this._ctx.font = this._characterFont;
// Measure the width of the character, but Math.floor it
// because that is what the renderer does when it calculates
// the character dimensions we are comparing against
const overlaps = Math.floor(this._ctx.measureText(chars).width) > this._characterWidth;
// Restore the original context
this._ctx.restore();
// Cache and return
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
* @param y The row of the char.
*/
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
+128
View File
@@ -0,0 +1,128 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export type CharacterJoinerHandler = (text: string) => [number, number][];
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRequestRedrawEvent {
start: number;
end: number;
}
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions;
/**
* Fires when the renderer is requesting to be redrawn on the next animation
* frame but is _not_ a result of content changing (eg. selection changes).
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
deregisterCharacterJoiner(joinerId: number): boolean;
}
export interface ICharacterJoiner {
id: number;
handler: CharacterJoinerHandler;
}
export interface ICharacterJoinerRegistry {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export interface IRenderLayer extends IDisposable {
/**
* Called when the terminal loses focus.
*/
onBlur(): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(): void;
/**
* Called when options change.
*/
onOptionsChanged(): void;
/**
* Called when the theme changes.
*/
setColors(colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
*/
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
/**
* Deregisters the specified character joiner handler
*/
deregisterCharacterJoiner?(joinerId: number): void;
/**
* Resize the render layer.
*/
resize(dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
*/
reset(): void;
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
import { IDisposable } from 'common/Types';
export abstract class BaseCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
public dispose(): void { }
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
private _doWarmUp(): void { }
/**
* Called when we start drawing a new frame.
*
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
* of BaseRenderLayer.
*/
public beginFrame(): void { }
/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
public abstract draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}
+95
View File
@@ -0,0 +1,95 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas';
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { IColorSet } from 'browser/Types';
import { ITerminalOptions } from 'common/services/Services';
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: number[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
*/
export function acquireCharAtlas(
options: ITerminalOptions,
rendererId: number,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors);
// Check to see if the renderer already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the renderer from the entry
if (entry.ownedBy.length === 1) {
entry.atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the renderer to the cache entry and return
entry.ownedBy.push(rendererId);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new DynamicCharAtlas(
document,
newConfig
),
config: newConfig,
ownedBy: [rendererId]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
*/
export function removeTerminalFromCache(rendererId: number): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only renderer
charAtlasCache[i].atlas.dispose();
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
+54
View File
@@ -0,0 +1,54 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { IColorSet, IPartialColorSet } from 'browser/Types';
import { ITerminalOptions } from 'common/services/Services';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: ITerminalOptions, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors = <IPartialColorSet>{
foreground: colors.foreground,
background: colors.background,
cursor: undefined,
cursorAccent: undefined,
selection: undefined,
ansi: colors.ansi
};
return {
devicePixelRatio: window.devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: options.fontFamily,
fontSize: options.fontSize,
fontWeight: options.fontWeight,
fontWeightBold: options.fontWeightBold,
allowTransparency: options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR;
}

Some files were not shown because too many files have changed in this diff Show More