webconfig: Rewrite color tab with Alpine.js

This commit is contained in:
Septatrix
2023-02-08 16:08:10 +01:00
parent ad90ae292d
commit fdce63f8ab
6 changed files with 373 additions and 291 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -395,7 +395,7 @@ body {
top: 0px;
}
.color_picker_background_cells div {
.color_picker_background_cells label {
width: 24px;
height: 24px;
border-style: solid;

View File

@@ -6,14 +6,16 @@
<title>fish shell configuration</title>
<link rel="icon" type="image/png" href="favicon.png" />
<link rel="stylesheet" type="text/css" href="fishconfig.css" />
<script type="text/javascript" src="js/colorutils.js" defer></script>
<script type="text/javascript" src="js/main.js" defer></script>
<script type="text/javascript" src="js/alpine.js" defer></script>
</head>
<!-- TODO check that every x-for has a :key binding -->
<!-- TODO check that every template has single child -->
<!-- TODO try to reduce depth by either splitting routes or eliminating redundant wrappers -->
<body id="ancestor" x-data="{ currentTab: 'functions' }">
<body id="ancestor" x-data="{ currentTab: 'colors' }">
<main id="parent">
<div id="tab_parent">
<a :class="{'tab': true, 'selected_tab': currentTab =='colors'}" id="tab_colors"
@@ -30,6 +32,163 @@
@click="currentTab = 'bindings'">bindings</a>
</div>
<div id="tab_contents">
<template x-if="currentTab === 'colors'" x-data="colors">
<div>
<!-- ko with: color_picker -->
<div class="colorpicker_text_sample" :style="{'background-color': terminalBackgroundColor}">
<span style="position: absolute; left: 10px; top: 3px"
:style="{'color': `#${text_color_for_color(terminalBackgroundColor)}`}"
x-text="selectedColorScheme.name"></span>
<br />
<div class="color_picker_background_cells">
<span style="display: block; text-align: right; line-height: 110%"
:style="{'color': `#${text_color_for_color(terminalBackgroundColor)}`}">
Background Color:<br />(demo only)&nbsp;
</span>
<template x-for="color in sampleTerminalBackgroundColors">
<label :style="{'background-color': color}">
<input type="radio" :value="color" x-model="terminalBackgroundColor" hidden
:title="'Preview with this background color.\n\nfish cannot change the background color of your terminal. Refer to your terminal settings to set its background color.'">
</input>
</label>
</template>
</div>
<!-- This is the sample text -->
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'command'}"
:style="{ 'color': interpret_color(selectedColorScheme.command)}"
@click="selectColorSetting('command')">/bright/vixens</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'param'}"
:style="{ 'color': interpret_color(selectedColorScheme.param)}"
@click="selectColorSetting('param')">jump</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'end'}"
:style="{ 'color': interpret_color(selectedColorScheme.end)}"
@click="selectColorSetting('end')">|</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'command'}"
:style="{ 'color': interpret_color(selectedColorScheme.command)}"
@click="selectColorSetting('command')">dozy</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'quote'}"
:style="{ 'color': interpret_color(selectedColorScheme.quote)}"
@click="selectColorSetting('quote')"> "fowl"</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'redirection'}"
:style="{ 'color': interpret_color(selectedColorScheme.redirection)}"
@click="selectColorSetting('redirection')">&gt; quack</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'end'}"
:style="{ 'color': interpret_color(selectedColorScheme.end)}"
@click="selectColorSetting('end')">&</span>
<br>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'command'}"
:style="{ 'color': interpret_color(selectedColorScheme.command)}"
@click="selectColorSetting('command')">echo</span>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'error'}"
:style="{ 'color': interpret_color(selectedColorScheme.error)}"
@click="selectColorSetting('error')">'</span><span
:class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'quote'}"
:style="{ 'color': interpret_color(selectedColorScheme.quote)}"
@click="selectColorSetting('quote')">Errors are the portals to discovery</span>
<br>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'comment'}"
:style="{ 'color': interpret_color(selectedColorScheme.comment)}"
@click="selectColorSetting('comment')"># This is a comment</span>
<br>
<span :class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'command'}"
:style="{ 'color': interpret_color(selectedColorScheme.command)}"
@click="selectColorSetting('command')">Th</span><span class="fake_cursor"><span
style="visibility: hidden">i</span></span><span
:class="{cs_clickable: customizationActive, cs_editing: csEditingType == 'autosuggestion'}"
:style="{ 'color': interpret_color(selectedColorScheme.autosuggestion)}"
@click="selectColorSetting('autosuggestion')">s is an autosuggestion</span>
<div style="position: absolute; right: 5px; bottom: 5px;">
<button class="customize_theme_button" :class="{button_highlight: customizationActive}"
:style="{'color': `#${text_color_for_color(terminalBackgroundColor)}`}"
@click="toggleCustomizationActive">Customize</button>
<button class="save_button"
:style="{'color': `#${text_color_for_color(terminalBackgroundColor)}`}"
x-show="showSaveButton" @click="setTheme" x-text="saveThemeButtonTitle"></button>
</div>
</div>
<div x-show="customizationActive">
<div style="margin: 10px 0 7px 35px;">Choose a color for <span
x-text="csUserVisibleTitle"></span>:</div>
<!-- TODO maybe add input[type=color] for 24bit terminals -->
<table class="colorpicker_term256" style="margin: 0px 20px;">
<tbody>
<template x-for="color_array in colorArraysArray">
<tr class="colorpicker_term256_row">
<template x-for="color in color_array">
<td class="colorpicker_term256_cell"
:style="{'background-color': interpret_color(color)}"
@click="changeSelectedTextColor(color)">
<div class="colorpicker_term256_selection_indicator"
x-show="selectedColorScheme[selectedColorSetting] == color"
:style="{'border-color': interpret_color(border_color_for_color(color))}">
</div>
</td>
</template>
</tr>
</template>
</tbody>
<!-- /ko -->
</table>
</div>
<div style="margin: 10px 0 7px 35px;">Preview a theme below:</div>
<div class="color_scheme_choices_scrollview">
<div class="color_scheme_choices_list">
<template x-for="colorScheme in colorSchemes">
<div class="color_scheme_choice_container"
@click="changeSelectedColorScheme(colorScheme)">
<div class="color_scheme_choice_label">
<!-- This click/clickBubble nonsense is so that we can have a separate URL inside a parent with an onClick handler -->
<span x-text="colorScheme.name"></span>
<a x-show="colorScheme.url" style="text-decoration: none; color: inherit;"
:href="colorScheme.url">&#10138;</a>
</div>
<div class="colorpicker_text_sample_tight"
:style="{'background-color': colorScheme.preferred_background}">
<span
:style="{'color': interpret_color(colorScheme.command)}">/bright/vixens</span>
<span :style="{'color': interpret_color(colorScheme.param)}">jump</span>
<span :style="{'color': interpret_color(colorScheme.end)}">|</span>
<span :style="{'color': interpret_color(colorScheme.command)}">dozy</span>
<span :style="{'color': interpret_color(colorScheme.quote)}"> "fowl" </span>
<span :style="{'color': interpret_color(colorScheme.redirection)}">&gt;
quack</span>
<span :style="{'color': interpret_color(colorScheme.end)}">&</span>
<br>
<span :style="{'color': interpret_color(colorScheme.command)}">echo</span>
<span :style="{'color': interpret_color(colorScheme.error)}">'</span><span
:style="{'color': interpret_color(colorScheme.quote)}">Errors are the
portals to discovery</span>
<br>
<span :style="{'color': interpret_color(colorScheme.comment)}"># This is a
comment</span>
<br>
<span :style="{'color': interpret_color(colorScheme.command)}">Th</span><span
class="fake_cursor"><span style="visibility: hidden">i</span></span><span
:style="{ 'color': interpret_color(colorScheme.autosuggestion)}">s is an
autosuggestion</span>
</div>
</div>
</template>
</div>
</div>
<!-- /ko -->
</div>
</template>
<!-- Investigate potential race condition with selectedPrompt (e.g. replace x-if with x-show) -->
<template x-if="currentTab === 'prompt'" x-data="prompt">
<div class="height_limiter">
@@ -79,6 +238,7 @@
<div class="master_detail_table">
<div class="master">
<template x-for="func in functions">
<!-- TODO use ul/li -->
<div>
<div id="master_{{func}}"
:class="{'master_element': true, 'selected_master_elem': func == selectedFunction }"

View File

@@ -1,79 +0,0 @@
fishconfig = angular.module("fishconfig", ["filters", "controllers", "ngRoute", "ngSanitize"]);
angular.module('fishconfig')
.filter('to_trusted', ['$sce', function($sce){
return function(text) {
return $sce.trustAsHtml(text);
};
}]);
fishconfig.config(
["$routeProvider", function($routeProvider) {
$routeProvider
.when("/colors", {
controller: "colorsController",
templateUrl: "partials/colors.html"
})
.when("/prompt", {
controller: "promptController",
templateUrl: "partials/prompt.html"
})
.when("/functions", {
controller: "functionsController",
templateUrl: "partials/functions.html"
})
.when("/variables", {
controller: "variablesController",
templateUrl: "partials/variables.html"
})
.when("/history", {
controller: "historyController",
templateUrl: "partials/history.html"
})
.when("/bindings", {
controller: "bindingsController",
templateUrl: "partials/bindings.html"
})
.otherwise({
redirectTo: "/colors"
})
}]);
/* Inspired from http://blog.tomaka17.com/2012/12/random-tricks-when-using-angularjs/ */
fishconfig.config(function($httpProvider, $compileProvider) {
var global_error_element = null;
var showMessage = function(content) {
global_error_element.text(content);
};
// $httpProvider.factory('myHttpInterceptor', function($q) {
// return {
// 'request' : function(promise) {
// return promise.then(function(successResponse) {
// showMessage('');
// return successResponse;
// })},
// 'requestError' : function(errorResponse) {
// switch (errorResponse.status) {
// case 0:
// showMessage("The request received an error. Perhaps the server has shut down.");
// break;
// case 500:
// showMessage('Server internal error: ' + errorResponse.data);
// break;
// default:
// showMessage('Error ' + errorResponse.status + ': ' + errorResponse.data);
// }
// return $q.reject(errorResponse);
// }
// }
// });
// $httpProvider.interceptors.push('myHttpInterceptor');
$compileProvider.directive('errorMessage', function() {
return {
link: function(scope, element, attrs) { global_error_element = element; }
};
});
});

View File

@@ -1,210 +0,0 @@
controllers = angular.module("controllers", []);
controllers.controller("main", function ($scope, $location) {
// substr(1) strips a leading slash
$scope.currentTab = $location.path().substr(1) || "colors";
$scope.changeView = function (view) {
$location.path(view);
$scope.currentTab = view;
};
});
controllers.controller("colorsController", function ($scope, $http) {
$scope.changeSelectedColorScheme = function (newScheme) {
$scope.selectedColorScheme = angular.copy(newScheme);
if ($scope.selectedColorScheme.preferred_background) {
$scope.terminalBackgroundColor = $scope.selectedColorScheme.preferred_background;
}
$scope.selectedColorSetting = false;
$scope.customizationActive = false;
$scope.csEditingType = false;
$scope.colorArraysArray = $scope.getColorArraysArray();
//TODO: Save button should be shown only when colors are changed
$scope.showSaveButton = true;
$scope.noteThemeChanged();
};
$scope.changeTerminalBackgroundColor = function (color) {
$scope.terminalBackgroundColor = color;
};
$scope.text_color_for_color = text_color_for_color;
$scope.border_color_for_color = border_color_for_color;
$scope.interpret_color = interpret_color;
$scope.getColorArraysArray = function () {
var result = null;
if ($scope.selectedColorScheme.colors && $scope.selectedColorScheme.colors.length > 0)
result = get_colors_as_nested_array($scope.selectedColorScheme.colors, 32);
else result = get_colors_as_nested_array(term_256_colors, 32);
return result;
};
$scope.beginCustomizationWithSetting = function (setting) {
if (!$scope.customizationActive) {
$scope.customizationActive = true;
$scope.selectedColorSetting = setting;
$scope.csEditingType = setting;
$scope.csUserVisibleTitle = user_visible_title_for_setting_name(setting);
}
};
$scope.selectColorSetting = function (name) {
$scope.selectedColorSetting = name;
$scope.csEditingType = $scope.customizationActive ? name : "";
$scope.csUserVisibleTitle = user_visible_title_for_setting_name(name);
$scope.beginCustomizationWithSetting(name);
};
$scope.toggleCustomizationActive = function () {
if (!$scope.customizationActive) {
$scope.beginCustomizationWithSetting($scope.selectedColorSetting || "command");
} else {
$scope.customizationActive = false;
$scope.selectedColorSetting = "";
$scope.csEditingType = "";
}
};
$scope.changeSelectedTextColor = function (color) {
$scope.selectedColorScheme[$scope.selectedColorSetting] = color;
$scope.selectedColorScheme["colordata-" + $scope.selectedColorSetting].color = color;
$scope.noteThemeChanged();
};
$scope.sampleTerminalBackgroundColors = [
"white",
"#" + solarized.base3,
"#300",
"#003",
"#" + solarized.base03,
"#232323",
"#" + nord.nord0,
"black",
];
/* Array of FishColorSchemes */
$scope.colorSchemes = [];
isValidColor = function (col) {
if (col == "normal") return true;
var s = new Option().style;
s.color = col;
return !!s.color;
};
$scope.getThemes = function () {
$http.get("colors/").then(function (arg) {
for (var scheme of arg.data) {
var currentScheme = { name: "Current", colors: [], preferred_background: "black" };
currentScheme["name"] = scheme["theme"];
if (scheme["name"]) currentScheme["name"] = scheme["name"];
var data = scheme["colors"];
if (scheme["preferred_background"]) {
if (isValidColor(scheme["preferred_background"])) {
currentScheme["preferred_background"] = scheme["preferred_background"];
}
}
if (scheme["url"]) currentScheme["url"] = scheme["url"];
for (var i in data) {
currentScheme[data[i].name] = interpret_color(data[i].color).replace(/#/, "");
// HACK: For some reason the colors array is cleared later
// So we cheesily encode the actual objects as colordata-, so we can send them.
// TODO: We should switch to keeping the objects, and also displaying them
// with underlines and such.
currentScheme["colordata-" + data[i].name] = data[i];
}
$scope.colorSchemes.push(currentScheme);
}
$scope.changeSelectedColorScheme($scope.colorSchemes[0]);
});
};
$scope.saveThemeButtonTitle = "Set Theme";
$scope.noteThemeChanged = function () {
$scope.saveThemeButtonTitle = "Set Theme";
};
$scope.setTheme = function () {
var settingNames = [
"normal",
"command",
"quote",
"redirection",
"end",
"error",
"param",
"comment",
"match",
"selection",
"search_match",
"history_current",
"operator",
"escape",
"cwd",
"cwd_root",
"valid_path",
"autosuggestion",
"user",
"host",
"cancel",
// Cheesy hardcoded variable names ahoy!
// These are all the pager vars,
// we should really just save all these in a dictionary.
"fish_pager_color_background",
"fish_pager_color_prefix",
"fish_pager_color_progress",
"fish_pager_color_completion",
"fish_pager_color_description",
"fish_pager_color_selected_background",
"fish_pager_color_selected_prefix",
"fish_pager_color_selected_completion",
"fish_pager_color_selected_description",
// TODO: Setting these to empty currently makes them weird. Figure out why!
/*
"fish_pager_color_secondary_background",
"fish_pager_color_secondary_prefix",
"fish_pager_color_secondary_completion",
"fish_pager_color_secondary_description",
*/
];
var remaining = settingNames.length;
var postdata = {
theme: $scope.selectedColorScheme["name"],
colors: [],
};
for (var name of settingNames) {
var selected;
var realname = "colordata-" + name;
// Skip colors undefined in the current theme
// js is dumb - the empty string is false,
// but we want that to mean unsetting a var.
if (
!$scope.selectedColorScheme[realname] &&
$scope.selectedColorScheme[realname] !== ""
) {
continue;
} else {
selected = $scope.selectedColorScheme[realname];
}
postdata.colors.push({
what: name,
color: selected,
});
}
$http
.post("set_color/", postdata, { headers: { "Content-Type": "application/json" } })
.then(function (arg) {
if (arg.status == 200) {
$scope.saveThemeButtonTitle = "Theme Set!";
}
});
};
$scope.getThemes();
});

View File

@@ -1,4 +1,215 @@
// TODO double check all defaults for nullability etc
document.addEventListener("alpine:init", () => {
window.Alpine.data("colors", () => ({
// TODO make null
selectedColorScheme: {},
terminalBackgroundColor: "black",
showSaveButton: false,
csUserVisibleTitle: "",
selectedColorSetting: false,
text_color_for_color: window.text_color_for_color,
changeSelectedColorScheme(newScheme) {
console.log(newScheme);
// TODO find out if angular.copy is deep copy
this.selectedColorScheme = { ...newScheme };
if (this.selectedColorScheme.preferred_background) {
this.terminalBackgroundColor = this.selectedColorScheme.preferred_background;
}
this.selectedColorSetting = false;
this.customizationActive = false;
this.csEditingType = false;
//TODO: Save button should be shown only when colors are changed
this.showSaveButton = true;
this.noteThemeChanged();
},
text_color_for_color: text_color_for_color,
border_color_for_color: border_color_for_color,
interpret_color: interpret_color,
get colorArraysArray() {
var result = null;
if (this.selectedColorScheme.colors && this.selectedColorScheme.colors.length > 0)
result = get_colors_as_nested_array(this.selectedColorScheme.colors, 32);
else result = get_colors_as_nested_array(term_256_colors, 32);
return result;
},
customizationActive: false,
csEditingType: false,
beginCustomizationWithSetting(setting) {
if (!this.customizationActive) {
this.customizationActive = true;
this.selectedColorSetting = setting;
this.csEditingType = setting;
this.csUserVisibleTitle = user_visible_title_for_setting_name(setting);
}
},
selectColorSetting(name) {
this.selectedColorSetting = name;
this.csEditingType = this.customizationActive ? name : "";
this.csUserVisibleTitle = user_visible_title_for_setting_name(name);
this.beginCustomizationWithSetting(name);
},
toggleCustomizationActive() {
if (!this.customizationActive) {
this.beginCustomizationWithSetting(this.selectedColorSetting || "command");
} else {
this.customizationActive = false;
this.selectedColorSetting = "";
this.csEditingType = "";
}
},
changeSelectedTextColor(color) {
this.selectedColorScheme[this.selectedColorSetting] = color;
this.selectedColorScheme["colordata-" + this.selectedColorSetting].color = color;
this.noteThemeChanged();
},
sampleTerminalBackgroundColors: [
"white",
"#" + solarized.base3,
"#300",
"#003",
"#" + solarized.base03,
"#232323",
"#" + nord.nord0,
"black",
],
/* Array of FishColorSchemes */
colorSchemes: [],
isValidColor(col) {
if (col == "normal") return true;
var s = new Option().style;
s.color = col;
return !!s.color;
},
saveThemeButtonTitle: "Set Theme",
noteThemeChanged() {
this.saveThemeButtonTitle = "Set Theme";
},
async setTheme() {
var settingNames = [
"normal",
"command",
"quote",
"redirection",
"end",
"error",
"param",
"comment",
"match",
"selection",
"search_match",
"history_current",
"operator",
"escape",
"cwd",
"cwd_root",
"valid_path",
"autosuggestion",
"user",
"host",
"cancel",
// Cheesy hardcoded variable names ahoy!
// These are all the pager vars,
// we should really just save all these in a dictionary.
"fish_pager_color_background",
"fish_pager_color_prefix",
"fish_pager_color_progress",
"fish_pager_color_completion",
"fish_pager_color_description",
"fish_pager_color_selected_background",
"fish_pager_color_selected_prefix",
"fish_pager_color_selected_completion",
"fish_pager_color_selected_description",
// TODO: Setting these to empty currently makes them weird. Figure out why!
/*
"fish_pager_color_secondary_background",
"fish_pager_color_secondary_prefix",
"fish_pager_color_secondary_completion",
"fish_pager_color_secondary_description",
*/
];
var remaining = settingNames.length;
var postdata = {
theme: this.selectedColorScheme["name"],
colors: [],
};
for (var name of settingNames) {
var selected;
var realname = "colordata-" + name;
// Skip colors undefined in the current theme
// js is dumb - the empty string is false,
// but we want that to mean unsetting a var.
if (
!this.selectedColorScheme[realname] &&
this.selectedColorScheme[realname] !== ""
) {
continue;
} else {
selected = this.selectedColorScheme[realname];
}
postdata.colors.push({
what: name,
color: selected,
});
}
let resp = await fetch("set_color/", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(postdata),
});
if (resp.ok) {
this.saveThemeButtonTitle = "Theme Set!";
}
},
async init() {
let schemes = await (await fetch("colors/")).json();
for (var scheme of schemes) {
var currentScheme = {
name: "Current",
colors: [],
preferred_background: "black",
};
currentScheme["name"] = scheme["theme"];
if (scheme["name"]) currentScheme["name"] = scheme["name"];
var data = scheme["colors"];
if (scheme["preferred_background"]) {
if (this.isValidColor(scheme["preferred_background"])) {
currentScheme["preferred_background"] = scheme["preferred_background"];
}
}
if (scheme["url"]) currentScheme["url"] = scheme["url"];
for (var i in data) {
currentScheme[data[i].name] = interpret_color(data[i].color).replace(/#/, "");
// HACK: For some reason the colors array is cleared later
// So we cheesily encode the actual objects as colordata-, so we can send them.
// TODO: We should switch to keeping the objects, and also displaying them
// with underlines and such.
currentScheme["colordata-" + data[i].name] = data[i];
}
this.colorSchemes.push(currentScheme);
}
this.changeSelectedColorScheme(this.colorSchemes[0]);
},
}));
window.Alpine.data("prompt", () => ({
selectedPrompt: null,
showSaveButton: true,