mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
deploy: 5c4fb01abf
This commit is contained in:
commit
0ce7d6618d
56 changed files with 9444 additions and 0 deletions
0
.nojekyll
Normal file
0
.nojekyll
Normal file
1
taskchampion/.nojekyll
Normal file
1
taskchampion/.nojekyll
Normal file
|
@ -0,0 +1 @@
|
||||||
|
This file makes sure that Github Pages doesn't process mdBook's output.
|
187
taskchampion/404.html
Normal file
187
taskchampion/404.html
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title></title>
|
||||||
|
<base href="/">
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="document-not-found-404"><a class="header" href="#document-not-found-404">Document not found (404)</a></h1>
|
||||||
|
<p>This URL is invalid, sorry. Please use the navigation bar or search to continue.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
4
taskchampion/FontAwesome/css/font-awesome.css
vendored
Normal file
4
taskchampion/FontAwesome/css/font-awesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
taskchampion/FontAwesome/fonts/FontAwesome.ttf
Normal file
BIN
taskchampion/FontAwesome/fonts/FontAwesome.ttf
Normal file
Binary file not shown.
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.eot
Normal file
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.eot
Normal file
Binary file not shown.
2671
taskchampion/FontAwesome/fonts/fontawesome-webfont.svg
Normal file
2671
taskchampion/FontAwesome/fonts/fontawesome-webfont.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 434 KiB |
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.ttf
Normal file
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.ttf
Normal file
Binary file not shown.
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.woff
Normal file
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.woff
Normal file
Binary file not shown.
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.woff2
Normal file
BIN
taskchampion/FontAwesome/fonts/fontawesome-webfont.woff2
Normal file
Binary file not shown.
79
taskchampion/ayu-highlight.css
Normal file
79
taskchampion/ayu-highlight.css
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
Based off of the Ayu theme
|
||||||
|
Original by Dempfi (https://github.com/dempfi/ayu)
|
||||||
|
*/
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #191f26;
|
||||||
|
color: #e6e1cf;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #5c6773;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #ff7733;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params {
|
||||||
|
color: #ffee99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: #b8cc52;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-section {
|
||||||
|
color: #ffb454;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-symbol {
|
||||||
|
color: #ff7733;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-name {
|
||||||
|
color: #36a3d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-tag {
|
||||||
|
color: #00568d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
color: #91b362;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #d96c75;
|
||||||
|
}
|
660
taskchampion/book.js
Normal file
660
taskchampion/book.js
Normal file
|
@ -0,0 +1,660 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Fix back button cache problem
|
||||||
|
window.onunload = function () { };
|
||||||
|
|
||||||
|
// Global variable, shared between modules
|
||||||
|
function playground_text(playground) {
|
||||||
|
let code_block = playground.querySelector("code");
|
||||||
|
|
||||||
|
if (window.ace && code_block.classList.contains("editable")) {
|
||||||
|
let editor = window.ace.edit(code_block);
|
||||||
|
return editor.getValue();
|
||||||
|
} else {
|
||||||
|
return code_block.textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(function codeSnippets() {
|
||||||
|
function fetch_with_timeout(url, options, timeout = 6000) {
|
||||||
|
return Promise.race([
|
||||||
|
fetch(url, options),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
var playgrounds = Array.from(document.querySelectorAll(".playground"));
|
||||||
|
if (playgrounds.length > 0) {
|
||||||
|
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': "application/json",
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => {
|
||||||
|
// get list of crates available in the rust playground
|
||||||
|
let playground_crates = response.crates.map(item => item["id"]);
|
||||||
|
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handle_crate_list_update(playground_block, playground_crates) {
|
||||||
|
// update the play buttons after receiving the response
|
||||||
|
update_play_button(playground_block, playground_crates);
|
||||||
|
|
||||||
|
// and install on change listener to dynamically update ACE editors
|
||||||
|
if (window.ace) {
|
||||||
|
let code_block = playground_block.querySelector("code");
|
||||||
|
if (code_block.classList.contains("editable")) {
|
||||||
|
let editor = window.ace.edit(code_block);
|
||||||
|
editor.addEventListener("change", function (e) {
|
||||||
|
update_play_button(playground_block, playground_crates);
|
||||||
|
});
|
||||||
|
// add Ctrl-Enter command to execute rust code
|
||||||
|
editor.commands.addCommand({
|
||||||
|
name: "run",
|
||||||
|
bindKey: {
|
||||||
|
win: "Ctrl-Enter",
|
||||||
|
mac: "Ctrl-Enter"
|
||||||
|
},
|
||||||
|
exec: _editor => run_rust_code(playground_block)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updates the visibility of play button based on `no_run` class and
|
||||||
|
// used crates vs ones available on http://play.rust-lang.org
|
||||||
|
function update_play_button(pre_block, playground_crates) {
|
||||||
|
var play_button = pre_block.querySelector(".play-button");
|
||||||
|
|
||||||
|
// skip if code is `no_run`
|
||||||
|
if (pre_block.querySelector('code').classList.contains("no_run")) {
|
||||||
|
play_button.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get list of `extern crate`'s from snippet
|
||||||
|
var txt = playground_text(pre_block);
|
||||||
|
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
|
||||||
|
var snippet_crates = [];
|
||||||
|
var item;
|
||||||
|
while (item = re.exec(txt)) {
|
||||||
|
snippet_crates.push(item[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if all used crates are available on play.rust-lang.org
|
||||||
|
var all_available = snippet_crates.every(function (elem) {
|
||||||
|
return playground_crates.indexOf(elem) > -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (all_available) {
|
||||||
|
play_button.classList.remove("hidden");
|
||||||
|
} else {
|
||||||
|
play_button.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_rust_code(code_block) {
|
||||||
|
var result_block = code_block.querySelector(".result");
|
||||||
|
if (!result_block) {
|
||||||
|
result_block = document.createElement('code');
|
||||||
|
result_block.className = 'result hljs language-bash';
|
||||||
|
|
||||||
|
code_block.append(result_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = playground_text(code_block);
|
||||||
|
let classes = code_block.querySelector('code').classList;
|
||||||
|
let has_2018 = classes.contains("edition2018");
|
||||||
|
let edition = has_2018 ? "2018" : "2015";
|
||||||
|
|
||||||
|
var params = {
|
||||||
|
version: "stable",
|
||||||
|
optimize: "0",
|
||||||
|
code: text,
|
||||||
|
edition: edition
|
||||||
|
};
|
||||||
|
|
||||||
|
if (text.indexOf("#![feature") !== -1) {
|
||||||
|
params.version = "nightly";
|
||||||
|
}
|
||||||
|
|
||||||
|
result_block.innerText = "Running...";
|
||||||
|
|
||||||
|
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': "application/json",
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'cors',
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(response => result_block.innerText = response.result)
|
||||||
|
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntax highlighting Configuration
|
||||||
|
hljs.configure({
|
||||||
|
tabReplace: ' ', // 4 spaces
|
||||||
|
languages: [], // Languages used for auto-detection
|
||||||
|
});
|
||||||
|
|
||||||
|
let code_nodes = Array
|
||||||
|
.from(document.querySelectorAll('code'))
|
||||||
|
// Don't highlight `inline code` blocks in headers.
|
||||||
|
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
|
||||||
|
|
||||||
|
if (window.ace) {
|
||||||
|
// language-rust class needs to be removed for editable
|
||||||
|
// blocks or highlightjs will capture events
|
||||||
|
Array
|
||||||
|
.from(document.querySelectorAll('code.editable'))
|
||||||
|
.forEach(function (block) { block.classList.remove('language-rust'); });
|
||||||
|
|
||||||
|
Array
|
||||||
|
.from(document.querySelectorAll('code:not(.editable)'))
|
||||||
|
.forEach(function (block) { hljs.highlightBlock(block); });
|
||||||
|
} else {
|
||||||
|
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adding the hljs class gives code blocks the color css
|
||||||
|
// even if highlighting doesn't apply
|
||||||
|
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
|
||||||
|
|
||||||
|
var lines = Array.from(block.querySelectorAll('.boring'));
|
||||||
|
// If no lines were hidden, return
|
||||||
|
if (!lines.length) { return; }
|
||||||
|
block.classList.add("hide-boring");
|
||||||
|
|
||||||
|
var buttons = document.createElement('div');
|
||||||
|
buttons.className = 'buttons';
|
||||||
|
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
|
||||||
|
|
||||||
|
// add expand button
|
||||||
|
var pre_block = block.parentNode;
|
||||||
|
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||||
|
|
||||||
|
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
|
||||||
|
if (e.target.classList.contains('fa-eye')) {
|
||||||
|
e.target.classList.remove('fa-eye');
|
||||||
|
e.target.classList.add('fa-eye-slash');
|
||||||
|
e.target.title = 'Hide lines';
|
||||||
|
e.target.setAttribute('aria-label', e.target.title);
|
||||||
|
|
||||||
|
block.classList.remove('hide-boring');
|
||||||
|
} else if (e.target.classList.contains('fa-eye-slash')) {
|
||||||
|
e.target.classList.remove('fa-eye-slash');
|
||||||
|
e.target.classList.add('fa-eye');
|
||||||
|
e.target.title = 'Show hidden lines';
|
||||||
|
e.target.setAttribute('aria-label', e.target.title);
|
||||||
|
|
||||||
|
block.classList.add('hide-boring');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.playground_copyable) {
|
||||||
|
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
|
||||||
|
var pre_block = block.parentNode;
|
||||||
|
if (!pre_block.classList.contains('playground')) {
|
||||||
|
var buttons = pre_block.querySelector(".buttons");
|
||||||
|
if (!buttons) {
|
||||||
|
buttons = document.createElement('div');
|
||||||
|
buttons.className = 'buttons';
|
||||||
|
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
var clipButton = document.createElement('button');
|
||||||
|
clipButton.className = 'fa fa-copy clip-button';
|
||||||
|
clipButton.title = 'Copy to clipboard';
|
||||||
|
clipButton.setAttribute('aria-label', clipButton.title);
|
||||||
|
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
|
||||||
|
|
||||||
|
buttons.insertBefore(clipButton, buttons.firstChild);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process playground code blocks
|
||||||
|
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
|
||||||
|
// Add play button
|
||||||
|
var buttons = pre_block.querySelector(".buttons");
|
||||||
|
if (!buttons) {
|
||||||
|
buttons = document.createElement('div');
|
||||||
|
buttons.className = 'buttons';
|
||||||
|
pre_block.insertBefore(buttons, pre_block.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
var runCodeButton = document.createElement('button');
|
||||||
|
runCodeButton.className = 'fa fa-play play-button';
|
||||||
|
runCodeButton.hidden = true;
|
||||||
|
runCodeButton.title = 'Run this code';
|
||||||
|
runCodeButton.setAttribute('aria-label', runCodeButton.title);
|
||||||
|
|
||||||
|
buttons.insertBefore(runCodeButton, buttons.firstChild);
|
||||||
|
runCodeButton.addEventListener('click', function (e) {
|
||||||
|
run_rust_code(pre_block);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.playground_copyable) {
|
||||||
|
var copyCodeClipboardButton = document.createElement('button');
|
||||||
|
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
|
||||||
|
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
|
||||||
|
copyCodeClipboardButton.title = 'Copy to clipboard';
|
||||||
|
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
|
||||||
|
|
||||||
|
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
|
||||||
|
}
|
||||||
|
|
||||||
|
let code_block = pre_block.querySelector("code");
|
||||||
|
if (window.ace && code_block.classList.contains("editable")) {
|
||||||
|
var undoChangesButton = document.createElement('button');
|
||||||
|
undoChangesButton.className = 'fa fa-history reset-button';
|
||||||
|
undoChangesButton.title = 'Undo changes';
|
||||||
|
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
|
||||||
|
|
||||||
|
buttons.insertBefore(undoChangesButton, buttons.firstChild);
|
||||||
|
|
||||||
|
undoChangesButton.addEventListener('click', function () {
|
||||||
|
let editor = window.ace.edit(code_block);
|
||||||
|
editor.setValue(editor.originalCode);
|
||||||
|
editor.clearSelection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function themes() {
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var themeToggleButton = document.getElementById('theme-toggle');
|
||||||
|
var themePopup = document.getElementById('theme-list');
|
||||||
|
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
|
||||||
|
var stylesheets = {
|
||||||
|
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
|
||||||
|
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
|
||||||
|
highlight: document.querySelector("[href$='highlight.css']"),
|
||||||
|
};
|
||||||
|
|
||||||
|
function showThemes() {
|
||||||
|
themePopup.style.display = 'block';
|
||||||
|
themeToggleButton.setAttribute('aria-expanded', true);
|
||||||
|
themePopup.querySelector("button#" + get_theme()).focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideThemes() {
|
||||||
|
themePopup.style.display = 'none';
|
||||||
|
themeToggleButton.setAttribute('aria-expanded', false);
|
||||||
|
themeToggleButton.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_theme() {
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
|
||||||
|
if (theme === null || theme === undefined) {
|
||||||
|
return default_theme;
|
||||||
|
} else {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_theme(theme, store = true) {
|
||||||
|
let ace_theme;
|
||||||
|
|
||||||
|
if (theme == 'coal' || theme == 'navy') {
|
||||||
|
stylesheets.ayuHighlight.disabled = true;
|
||||||
|
stylesheets.tomorrowNight.disabled = false;
|
||||||
|
stylesheets.highlight.disabled = true;
|
||||||
|
|
||||||
|
ace_theme = "ace/theme/tomorrow_night";
|
||||||
|
} else if (theme == 'ayu') {
|
||||||
|
stylesheets.ayuHighlight.disabled = false;
|
||||||
|
stylesheets.tomorrowNight.disabled = true;
|
||||||
|
stylesheets.highlight.disabled = true;
|
||||||
|
ace_theme = "ace/theme/tomorrow_night";
|
||||||
|
} else {
|
||||||
|
stylesheets.ayuHighlight.disabled = true;
|
||||||
|
stylesheets.tomorrowNight.disabled = true;
|
||||||
|
stylesheets.highlight.disabled = false;
|
||||||
|
ace_theme = "ace/theme/dawn";
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
|
||||||
|
}, 1);
|
||||||
|
|
||||||
|
if (window.ace && window.editors) {
|
||||||
|
window.editors.forEach(function (editor) {
|
||||||
|
editor.setTheme(ace_theme);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousTheme = get_theme();
|
||||||
|
|
||||||
|
if (store) {
|
||||||
|
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
html.classList.remove(previousTheme);
|
||||||
|
html.classList.add(theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set theme
|
||||||
|
var theme = get_theme();
|
||||||
|
|
||||||
|
set_theme(theme, false);
|
||||||
|
|
||||||
|
themeToggleButton.addEventListener('click', function () {
|
||||||
|
if (themePopup.style.display === 'block') {
|
||||||
|
hideThemes();
|
||||||
|
} else {
|
||||||
|
showThemes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
themePopup.addEventListener('click', function (e) {
|
||||||
|
var theme = e.target.id || e.target.parentElement.id;
|
||||||
|
set_theme(theme);
|
||||||
|
});
|
||||||
|
|
||||||
|
themePopup.addEventListener('focusout', function(e) {
|
||||||
|
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
|
||||||
|
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
|
||||||
|
hideThemes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
|
||||||
|
hideThemes();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||||
|
if (!themePopup.contains(e.target)) { return; }
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Escape':
|
||||||
|
e.preventDefault();
|
||||||
|
hideThemes();
|
||||||
|
break;
|
||||||
|
case 'ArrowUp':
|
||||||
|
e.preventDefault();
|
||||||
|
var li = document.activeElement.parentElement;
|
||||||
|
if (li && li.previousElementSibling) {
|
||||||
|
li.previousElementSibling.querySelector('button').focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
e.preventDefault();
|
||||||
|
var li = document.activeElement.parentElement;
|
||||||
|
if (li && li.nextElementSibling) {
|
||||||
|
li.nextElementSibling.querySelector('button').focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Home':
|
||||||
|
e.preventDefault();
|
||||||
|
themePopup.querySelector('li:first-child button').focus();
|
||||||
|
break;
|
||||||
|
case 'End':
|
||||||
|
e.preventDefault();
|
||||||
|
themePopup.querySelector('li:last-child button').focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function sidebar() {
|
||||||
|
var html = document.querySelector("html");
|
||||||
|
var sidebar = document.getElementById("sidebar");
|
||||||
|
var sidebarLinks = document.querySelectorAll('#sidebar a');
|
||||||
|
var sidebarToggleButton = document.getElementById("sidebar-toggle");
|
||||||
|
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
|
||||||
|
var firstContact = null;
|
||||||
|
|
||||||
|
function showSidebar() {
|
||||||
|
html.classList.remove('sidebar-hidden')
|
||||||
|
html.classList.add('sidebar-visible');
|
||||||
|
Array.from(sidebarLinks).forEach(function (link) {
|
||||||
|
link.setAttribute('tabIndex', 0);
|
||||||
|
});
|
||||||
|
sidebarToggleButton.setAttribute('aria-expanded', true);
|
||||||
|
sidebar.setAttribute('aria-hidden', false);
|
||||||
|
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
|
||||||
|
|
||||||
|
function toggleSection(ev) {
|
||||||
|
ev.currentTarget.parentElement.classList.toggle('expanded');
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(sidebarAnchorToggles).forEach(function (el) {
|
||||||
|
el.addEventListener('click', toggleSection);
|
||||||
|
});
|
||||||
|
|
||||||
|
function hideSidebar() {
|
||||||
|
html.classList.remove('sidebar-visible')
|
||||||
|
html.classList.add('sidebar-hidden');
|
||||||
|
Array.from(sidebarLinks).forEach(function (link) {
|
||||||
|
link.setAttribute('tabIndex', -1);
|
||||||
|
});
|
||||||
|
sidebarToggleButton.setAttribute('aria-expanded', false);
|
||||||
|
sidebar.setAttribute('aria-hidden', true);
|
||||||
|
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle sidebar
|
||||||
|
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
|
||||||
|
if (html.classList.contains("sidebar-hidden")) {
|
||||||
|
var current_width = parseInt(
|
||||||
|
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
|
||||||
|
if (current_width < 150) {
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', '150px');
|
||||||
|
}
|
||||||
|
showSidebar();
|
||||||
|
} else if (html.classList.contains("sidebar-visible")) {
|
||||||
|
hideSidebar();
|
||||||
|
} else {
|
||||||
|
if (getComputedStyle(sidebar)['transform'] === 'none') {
|
||||||
|
hideSidebar();
|
||||||
|
} else {
|
||||||
|
showSidebar();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
|
||||||
|
|
||||||
|
function initResize(e) {
|
||||||
|
window.addEventListener('mousemove', resize, false);
|
||||||
|
window.addEventListener('mouseup', stopResize, false);
|
||||||
|
html.classList.add('sidebar-resizing');
|
||||||
|
}
|
||||||
|
function resize(e) {
|
||||||
|
var pos = (e.clientX - sidebar.offsetLeft);
|
||||||
|
if (pos < 20) {
|
||||||
|
hideSidebar();
|
||||||
|
} else {
|
||||||
|
if (html.classList.contains("sidebar-hidden")) {
|
||||||
|
showSidebar();
|
||||||
|
}
|
||||||
|
pos = Math.min(pos, window.innerWidth - 100);
|
||||||
|
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//on mouseup remove windows functions mousemove & mouseup
|
||||||
|
function stopResize(e) {
|
||||||
|
html.classList.remove('sidebar-resizing');
|
||||||
|
window.removeEventListener('mousemove', resize, false);
|
||||||
|
window.removeEventListener('mouseup', stopResize, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('touchstart', function (e) {
|
||||||
|
firstContact = {
|
||||||
|
x: e.touches[0].clientX,
|
||||||
|
time: Date.now()
|
||||||
|
};
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', function (e) {
|
||||||
|
if (!firstContact)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var curX = e.touches[0].clientX;
|
||||||
|
var xDiff = curX - firstContact.x,
|
||||||
|
tDiff = Date.now() - firstContact.time;
|
||||||
|
|
||||||
|
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
|
||||||
|
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
|
||||||
|
showSidebar();
|
||||||
|
else if (xDiff < 0 && curX < 300)
|
||||||
|
hideSidebar();
|
||||||
|
|
||||||
|
firstContact = null;
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
// Scroll sidebar to current active section
|
||||||
|
var activeSection = document.getElementById("sidebar").querySelector(".active");
|
||||||
|
if (activeSection) {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
|
||||||
|
activeSection.scrollIntoView({ block: 'center' });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function chapterNavigation() {
|
||||||
|
document.addEventListener('keydown', function (e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
|
||||||
|
if (window.search && window.search.hasFocus()) { return; }
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight':
|
||||||
|
e.preventDefault();
|
||||||
|
var nextButton = document.querySelector('.nav-chapters.next');
|
||||||
|
if (nextButton) {
|
||||||
|
window.location.href = nextButton.href;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowLeft':
|
||||||
|
e.preventDefault();
|
||||||
|
var previousButton = document.querySelector('.nav-chapters.previous');
|
||||||
|
if (previousButton) {
|
||||||
|
window.location.href = previousButton.href;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function clipboard() {
|
||||||
|
var clipButtons = document.querySelectorAll('.clip-button');
|
||||||
|
|
||||||
|
function hideTooltip(elem) {
|
||||||
|
elem.firstChild.innerText = "";
|
||||||
|
elem.className = 'fa fa-copy clip-button';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showTooltip(elem, msg) {
|
||||||
|
elem.firstChild.innerText = msg;
|
||||||
|
elem.className = 'fa fa-copy tooltipped';
|
||||||
|
}
|
||||||
|
|
||||||
|
var clipboardSnippets = new ClipboardJS('.clip-button', {
|
||||||
|
text: function (trigger) {
|
||||||
|
hideTooltip(trigger);
|
||||||
|
let playground = trigger.closest("pre");
|
||||||
|
return playground_text(playground);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Array.from(clipButtons).forEach(function (clipButton) {
|
||||||
|
clipButton.addEventListener('mouseout', function (e) {
|
||||||
|
hideTooltip(e.currentTarget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboardSnippets.on('success', function (e) {
|
||||||
|
e.clearSelection();
|
||||||
|
showTooltip(e.trigger, "Copied!");
|
||||||
|
});
|
||||||
|
|
||||||
|
clipboardSnippets.on('error', function (e) {
|
||||||
|
showTooltip(e.trigger, "Clipboard error!");
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function scrollToTop () {
|
||||||
|
var menuTitle = document.querySelector('.menu-title');
|
||||||
|
|
||||||
|
menuTitle.addEventListener('click', function () {
|
||||||
|
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
(function controllMenu() {
|
||||||
|
var menu = document.getElementById('menu-bar');
|
||||||
|
|
||||||
|
(function controllPosition() {
|
||||||
|
var scrollTop = document.scrollingElement.scrollTop;
|
||||||
|
var prevScrollTop = scrollTop;
|
||||||
|
var minMenuY = -menu.clientHeight - 50;
|
||||||
|
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
|
||||||
|
menu.style.top = scrollTop + 'px';
|
||||||
|
// Same as parseInt(menu.style.top.slice(0, -2), but faster
|
||||||
|
var topCache = menu.style.top.slice(0, -2);
|
||||||
|
menu.classList.remove('sticky');
|
||||||
|
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
|
||||||
|
document.addEventListener('scroll', function () {
|
||||||
|
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
|
||||||
|
// `null` means that it doesn't need to be updated
|
||||||
|
var nextSticky = null;
|
||||||
|
var nextTop = null;
|
||||||
|
var scrollDown = scrollTop > prevScrollTop;
|
||||||
|
var menuPosAbsoluteY = topCache - scrollTop;
|
||||||
|
if (scrollDown) {
|
||||||
|
nextSticky = false;
|
||||||
|
if (menuPosAbsoluteY > 0) {
|
||||||
|
nextTop = prevScrollTop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (menuPosAbsoluteY > 0) {
|
||||||
|
nextSticky = true;
|
||||||
|
} else if (menuPosAbsoluteY < minMenuY) {
|
||||||
|
nextTop = prevScrollTop + minMenuY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextSticky === true && stickyCache === false) {
|
||||||
|
menu.classList.add('sticky');
|
||||||
|
stickyCache = true;
|
||||||
|
} else if (nextSticky === false && stickyCache === true) {
|
||||||
|
menu.classList.remove('sticky');
|
||||||
|
stickyCache = false;
|
||||||
|
}
|
||||||
|
if (nextTop !== null) {
|
||||||
|
menu.style.top = nextTop + 'px';
|
||||||
|
topCache = nextTop;
|
||||||
|
}
|
||||||
|
prevScrollTop = scrollTop;
|
||||||
|
}, { passive: true });
|
||||||
|
})();
|
||||||
|
(function controllBorder() {
|
||||||
|
menu.classList.remove('bordered');
|
||||||
|
document.addEventListener('scroll', function () {
|
||||||
|
if (menu.offsetTop === 0) {
|
||||||
|
menu.classList.remove('bordered');
|
||||||
|
} else {
|
||||||
|
menu.classList.add('bordered');
|
||||||
|
}
|
||||||
|
}, { passive: true });
|
||||||
|
})();
|
||||||
|
})();
|
7
taskchampion/clipboard.min.js
vendored
Normal file
7
taskchampion/clipboard.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
495
taskchampion/css/chrome.css
Normal file
495
taskchampion/css/chrome.css
Normal file
|
@ -0,0 +1,495 @@
|
||||||
|
/* CSS for UI elements (a.k.a. chrome) */
|
||||||
|
|
||||||
|
@import 'variables.css';
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar);
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scrollbar-color: var(--scrollbar) var(--bg);
|
||||||
|
}
|
||||||
|
#searchresults a,
|
||||||
|
.content a:link,
|
||||||
|
a:visited,
|
||||||
|
a > .hljs {
|
||||||
|
color: var(--links);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Menu Bar */
|
||||||
|
|
||||||
|
#menu-bar,
|
||||||
|
#menu-bar-hover-placeholder {
|
||||||
|
z-index: 101;
|
||||||
|
margin: auto calc(0px - var(--page-padding));
|
||||||
|
}
|
||||||
|
#menu-bar {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
background-color: var(--bg);
|
||||||
|
border-bottom-color: var(--bg);
|
||||||
|
border-bottom-width: 1px;
|
||||||
|
border-bottom-style: solid;
|
||||||
|
}
|
||||||
|
#menu-bar.sticky,
|
||||||
|
.js #menu-bar-hover-placeholder:hover + #menu-bar,
|
||||||
|
.js #menu-bar:hover,
|
||||||
|
.js.sidebar-visible #menu-bar {
|
||||||
|
position: -webkit-sticky;
|
||||||
|
position: sticky;
|
||||||
|
top: 0 !important;
|
||||||
|
}
|
||||||
|
#menu-bar-hover-placeholder {
|
||||||
|
position: sticky;
|
||||||
|
position: -webkit-sticky;
|
||||||
|
top: 0;
|
||||||
|
height: var(--menu-bar-height);
|
||||||
|
}
|
||||||
|
#menu-bar.bordered {
|
||||||
|
border-bottom-color: var(--table-border-color);
|
||||||
|
}
|
||||||
|
#menu-bar i, #menu-bar .icon-button {
|
||||||
|
position: relative;
|
||||||
|
padding: 0 8px;
|
||||||
|
z-index: 10;
|
||||||
|
line-height: var(--menu-bar-height);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.5s;
|
||||||
|
}
|
||||||
|
@media only screen and (max-width: 420px) {
|
||||||
|
#menu-bar i, #menu-bar .icon-button {
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.icon-button i {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-buttons {
|
||||||
|
margin: 0 15px;
|
||||||
|
}
|
||||||
|
.right-buttons a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-buttons {
|
||||||
|
display: flex;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
.no-js .left-buttons {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-title {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: 200;
|
||||||
|
font-size: 2.4rem;
|
||||||
|
line-height: var(--menu-bar-height);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.js .menu-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bar,
|
||||||
|
.menu-bar:visited,
|
||||||
|
.nav-chapters,
|
||||||
|
.nav-chapters:visited,
|
||||||
|
.mobile-nav-chapters,
|
||||||
|
.mobile-nav-chapters:visited,
|
||||||
|
.menu-bar .icon-button,
|
||||||
|
.menu-bar a i {
|
||||||
|
color: var(--icons);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bar i:hover,
|
||||||
|
.menu-bar .icon-button:hover,
|
||||||
|
.nav-chapters:hover,
|
||||||
|
.mobile-nav-chapters i:hover {
|
||||||
|
color: var(--icons-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nav Icons */
|
||||||
|
|
||||||
|
.nav-chapters {
|
||||||
|
font-size: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 150px;
|
||||||
|
min-width: 90px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
transition: color 0.5s, background-color 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-chapters:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: var(--theme-hover);
|
||||||
|
transition: background-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-wrapper {
|
||||||
|
margin-top: 50px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-nav-chapters {
|
||||||
|
font-size: 2.5em;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 90px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: var(--sidebar-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.previous {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.next {
|
||||||
|
float: right;
|
||||||
|
right: var(--page-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1080px) {
|
||||||
|
.nav-wide-wrapper { display: none; }
|
||||||
|
.nav-wrapper { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 1380px) {
|
||||||
|
.sidebar-visible .nav-wide-wrapper { display: none; }
|
||||||
|
.sidebar-visible .nav-wrapper { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline code */
|
||||||
|
|
||||||
|
:not(pre) > .hljs {
|
||||||
|
display: inline;
|
||||||
|
padding: 0.1em 0.3em;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(pre):not(a) > .hljs {
|
||||||
|
color: var(--inline-code-color);
|
||||||
|
overflow-x: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover > .hljs {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
pre > .buttons {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
right: 5px;
|
||||||
|
top: 5px;
|
||||||
|
|
||||||
|
color: var(--sidebar-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
pre > .buttons :hover {
|
||||||
|
color: var(--sidebar-active);
|
||||||
|
}
|
||||||
|
pre > .buttons i {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
pre > .buttons button {
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: inherit;
|
||||||
|
}
|
||||||
|
pre > .result {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search */
|
||||||
|
|
||||||
|
#searchresults a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 3px 1px 3px;
|
||||||
|
margin: 0 -3px -1px -3px;
|
||||||
|
background-color: var(--search-mark-bg);
|
||||||
|
transition: background-color 300ms linear;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark.fade-out {
|
||||||
|
background-color: rgba(0,0,0,0) !important;
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchbar-outer {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: var(--content-max-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbar {
|
||||||
|
width: 100%;
|
||||||
|
margin: 5px auto 0px auto;
|
||||||
|
padding: 10px 16px;
|
||||||
|
transition: box-shadow 300ms ease-in-out;
|
||||||
|
border: 1px solid var(--searchbar-border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: var(--searchbar-bg);
|
||||||
|
color: var(--searchbar-fg);
|
||||||
|
}
|
||||||
|
#searchbar:focus,
|
||||||
|
#searchbar.active {
|
||||||
|
box-shadow: 0 0 3px var(--searchbar-shadow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-header {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1em;
|
||||||
|
padding: 18px 0 0 5px;
|
||||||
|
color: var(--searchresults-header-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchresults-outer {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: var(--content-max-width);
|
||||||
|
border-bottom: 1px dashed var(--searchresults-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#searchresults {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
ul#searchresults li {
|
||||||
|
margin: 10px 0px;
|
||||||
|
padding: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
ul#searchresults li.focus {
|
||||||
|
background-color: var(--searchresults-li-bg);
|
||||||
|
}
|
||||||
|
ul#searchresults span.teaser {
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
margin: 5px 0 0 20px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
ul#searchresults span.teaser em {
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: var(--sidebar-width);
|
||||||
|
font-size: 0.875em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
overscroll-behavior-y: contain;
|
||||||
|
background-color: var(--sidebar-bg);
|
||||||
|
color: var(--sidebar-fg);
|
||||||
|
}
|
||||||
|
.sidebar-resizing {
|
||||||
|
-moz-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.js:not(.sidebar-resizing) .sidebar {
|
||||||
|
transition: transform 0.3s; /* Animation: slide away */
|
||||||
|
}
|
||||||
|
.sidebar code {
|
||||||
|
line-height: 2em;
|
||||||
|
}
|
||||||
|
.sidebar .sidebar-scrollbox {
|
||||||
|
overflow-y: auto;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 10px 10px;
|
||||||
|
}
|
||||||
|
.sidebar .sidebar-resize-handle {
|
||||||
|
position: absolute;
|
||||||
|
cursor: col-resize;
|
||||||
|
width: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.js .sidebar .sidebar-resize-handle {
|
||||||
|
cursor: col-resize;
|
||||||
|
width: 5px;
|
||||||
|
}
|
||||||
|
.sidebar-hidden .sidebar {
|
||||||
|
transform: translateX(calc(0px - var(--sidebar-width)));
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar {
|
||||||
|
background: var(--sidebar-bg);
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--scrollbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-visible .page-wrapper {
|
||||||
|
transform: translateX(var(--sidebar-width));
|
||||||
|
}
|
||||||
|
@media only screen and (min-width: 620px) {
|
||||||
|
.sidebar-visible .page-wrapper {
|
||||||
|
transform: none;
|
||||||
|
margin-left: var(--sidebar-width);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter {
|
||||||
|
list-style: none outside none;
|
||||||
|
padding-left: 0;
|
||||||
|
line-height: 2.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter ol {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li {
|
||||||
|
display: flex;
|
||||||
|
color: var(--sidebar-non-existant);
|
||||||
|
}
|
||||||
|
.chapter li a {
|
||||||
|
display: block;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--sidebar-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li a:hover {
|
||||||
|
color: var(--sidebar-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li a.active {
|
||||||
|
color: var(--sidebar-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li > a.toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0 10px;
|
||||||
|
user-select: none;
|
||||||
|
opacity: 0.68;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li > a.toggle div {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* collapse the section */
|
||||||
|
.chapter li:not(.expanded) + li > ol {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li.chapter-item {
|
||||||
|
line-height: 1.5em;
|
||||||
|
margin-top: 0.6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li.expanded > a.toggle div {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
width: 100%;
|
||||||
|
height: 3px;
|
||||||
|
margin: 5px 0px;
|
||||||
|
}
|
||||||
|
.chapter .spacer {
|
||||||
|
background-color: var(--sidebar-spacer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (-moz-touch-enabled: 1), (pointer: coarse) {
|
||||||
|
.chapter li a { padding: 5px 0; }
|
||||||
|
.spacer { margin: 10px 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
list-style: none outside none;
|
||||||
|
padding-left: 20px;
|
||||||
|
line-height: 1.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Menu Popup */
|
||||||
|
|
||||||
|
.theme-popup {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: var(--menu-bar-height);
|
||||||
|
z-index: 1000;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.7em;
|
||||||
|
color: var(--fg);
|
||||||
|
background: var(--theme-popup-bg);
|
||||||
|
border: 1px solid var(--theme-popup-border);
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.theme-popup .default {
|
||||||
|
color: var(--icons);
|
||||||
|
}
|
||||||
|
.theme-popup .theme {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2px 10px;
|
||||||
|
line-height: 25px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: inherit;
|
||||||
|
background: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
.theme-popup .theme:hover {
|
||||||
|
background-color: var(--theme-hover);
|
||||||
|
}
|
||||||
|
.theme-popup .theme:hover:first-child,
|
||||||
|
.theme-popup .theme:hover:last-child {
|
||||||
|
border-top-left-radius: inherit;
|
||||||
|
border-top-right-radius: inherit;
|
||||||
|
}
|
177
taskchampion/css/general.css
Normal file
177
taskchampion/css/general.css
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
/* Base styles and content styles */
|
||||||
|
|
||||||
|
@import 'variables.css';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Browser default font-size is 16px, this way 1 rem = 10px */
|
||||||
|
font-size: 62.5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--bg);
|
||||||
|
text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.6rem;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
|
||||||
|
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Don't change font size in headers. */
|
||||||
|
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
|
||||||
|
font-size: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left { float: left; }
|
||||||
|
.right { float: right; }
|
||||||
|
.boring { opacity: 0.6; }
|
||||||
|
.hide-boring .boring { display: none; }
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
|
h2, h3 { margin-top: 2.5em; }
|
||||||
|
h4, h5 { margin-top: 2em; }
|
||||||
|
|
||||||
|
.header + .header h3,
|
||||||
|
.header + .header h4,
|
||||||
|
.header + .header h5 {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1:target::before,
|
||||||
|
h2:target::before,
|
||||||
|
h3:target::before,
|
||||||
|
h4:target::before,
|
||||||
|
h5:target::before,
|
||||||
|
h6:target::before {
|
||||||
|
display: inline-block;
|
||||||
|
content: "»";
|
||||||
|
margin-left: -30px;
|
||||||
|
width: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This is broken on Safari as of version 14, but is fixed
|
||||||
|
in Safari Technology Preview 117 which I think will be Safari 14.2.
|
||||||
|
https://bugs.webkit.org/show_bug.cgi?id=218076
|
||||||
|
*/
|
||||||
|
:target {
|
||||||
|
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
outline: 0;
|
||||||
|
padding: 0 var(--page-padding);
|
||||||
|
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
|
||||||
|
}
|
||||||
|
.page-wrapper {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.js:not(.sidebar-resizing) .page-wrapper {
|
||||||
|
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 0 15px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
}
|
||||||
|
.content main {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: var(--content-max-width);
|
||||||
|
}
|
||||||
|
.content p { line-height: 1.45em; }
|
||||||
|
.content ol { line-height: 1.45em; }
|
||||||
|
.content ul { line-height: 1.45em; }
|
||||||
|
.content a { text-decoration: none; }
|
||||||
|
.content a:hover { text-decoration: underline; }
|
||||||
|
.content img, .content video { max-width: 100%; }
|
||||||
|
.content .header:link,
|
||||||
|
.content .header:visited {
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
.content .header:link,
|
||||||
|
.content .header:visited:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: 0 auto;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table td {
|
||||||
|
padding: 3px 20px;
|
||||||
|
border: 1px var(--table-border-color) solid;
|
||||||
|
}
|
||||||
|
table thead {
|
||||||
|
background: var(--table-header-bg);
|
||||||
|
}
|
||||||
|
table thead td {
|
||||||
|
font-weight: 700;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
table thead th {
|
||||||
|
padding: 3px 20px;
|
||||||
|
}
|
||||||
|
table thead tr {
|
||||||
|
border: 1px var(--table-header-bg) solid;
|
||||||
|
}
|
||||||
|
/* Alternate background colors for rows */
|
||||||
|
table tbody tr:nth-child(2n) {
|
||||||
|
background: var(--table-alternate-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
color: var(--fg);
|
||||||
|
background-color: var(--quote-bg);
|
||||||
|
border-top: .1em solid var(--quote-border);
|
||||||
|
border-bottom: .1em solid var(--quote-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
:not(.footnote-definition) + .footnote-definition,
|
||||||
|
.footnote-definition + :not(.footnote-definition) {
|
||||||
|
margin-top: 2em;
|
||||||
|
}
|
||||||
|
.footnote-definition {
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
|
.footnote-definition p {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltiptext {
|
||||||
|
position: absolute;
|
||||||
|
visibility: hidden;
|
||||||
|
color: #fff;
|
||||||
|
background-color: #333;
|
||||||
|
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
|
||||||
|
left: -8px; /* Half of the width of the icon */
|
||||||
|
top: -35px;
|
||||||
|
font-size: 0.8em;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
margin: 5px;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.tooltipped .tooltiptext {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter li.part-title {
|
||||||
|
color: var(--sidebar-fg);
|
||||||
|
margin: 5px 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
54
taskchampion/css/print.css
Normal file
54
taskchampion/css/print.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
#sidebar,
|
||||||
|
#menu-bar,
|
||||||
|
.nav-chapters,
|
||||||
|
.mobile-nav-chapters {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-wrapper.page-wrapper {
|
||||||
|
transform: none;
|
||||||
|
margin-left: 0px;
|
||||||
|
overflow-y: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content {
|
||||||
|
max-width: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page {
|
||||||
|
overflow-y: initial;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: #666666;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
/* Force background to be printed in Chrome */
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > .buttons {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, a:visited, a:active, a:hover {
|
||||||
|
color: #4183c4;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa {
|
||||||
|
display: none !important;
|
||||||
|
}
|
253
taskchampion/css/variables.css
Normal file
253
taskchampion/css/variables.css
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
|
||||||
|
/* Globals */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--sidebar-width: 300px;
|
||||||
|
--page-padding: 15px;
|
||||||
|
--content-max-width: 750px;
|
||||||
|
--menu-bar-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Themes */
|
||||||
|
|
||||||
|
.ayu {
|
||||||
|
--bg: hsl(210, 25%, 8%);
|
||||||
|
--fg: #c5c5c5;
|
||||||
|
|
||||||
|
--sidebar-bg: #14191f;
|
||||||
|
--sidebar-fg: #c8c9db;
|
||||||
|
--sidebar-non-existant: #5c6773;
|
||||||
|
--sidebar-active: #ffb454;
|
||||||
|
--sidebar-spacer: #2d334f;
|
||||||
|
|
||||||
|
--scrollbar: var(--sidebar-fg);
|
||||||
|
|
||||||
|
--icons: #737480;
|
||||||
|
--icons-hover: #b7b9cc;
|
||||||
|
|
||||||
|
--links: #0096cf;
|
||||||
|
|
||||||
|
--inline-code-color: #ffb454;
|
||||||
|
|
||||||
|
--theme-popup-bg: #14191f;
|
||||||
|
--theme-popup-border: #5c6773;
|
||||||
|
--theme-hover: #191f26;
|
||||||
|
|
||||||
|
--quote-bg: hsl(226, 15%, 17%);
|
||||||
|
--quote-border: hsl(226, 15%, 22%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(210, 25%, 13%);
|
||||||
|
--table-header-bg: hsl(210, 25%, 28%);
|
||||||
|
--table-alternate-bg: hsl(210, 25%, 11%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #848484;
|
||||||
|
--searchbar-bg: #424242;
|
||||||
|
--searchbar-fg: #fff;
|
||||||
|
--searchbar-shadow-color: #d4c89f;
|
||||||
|
--searchresults-header-fg: #666;
|
||||||
|
--searchresults-border-color: #888;
|
||||||
|
--searchresults-li-bg: #252932;
|
||||||
|
--search-mark-bg: #e3b171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coal {
|
||||||
|
--bg: hsl(200, 7%, 8%);
|
||||||
|
--fg: #98a3ad;
|
||||||
|
|
||||||
|
--sidebar-bg: #292c2f;
|
||||||
|
--sidebar-fg: #a1adb8;
|
||||||
|
--sidebar-non-existant: #505254;
|
||||||
|
--sidebar-active: #3473ad;
|
||||||
|
--sidebar-spacer: #393939;
|
||||||
|
|
||||||
|
--scrollbar: var(--sidebar-fg);
|
||||||
|
|
||||||
|
--icons: #43484d;
|
||||||
|
--icons-hover: #b3c0cc;
|
||||||
|
|
||||||
|
--links: #2b79a2;
|
||||||
|
|
||||||
|
--inline-code-color: #c5c8c6;;
|
||||||
|
|
||||||
|
--theme-popup-bg: #141617;
|
||||||
|
--theme-popup-border: #43484d;
|
||||||
|
--theme-hover: #1f2124;
|
||||||
|
|
||||||
|
--quote-bg: hsl(234, 21%, 18%);
|
||||||
|
--quote-border: hsl(234, 21%, 23%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(200, 7%, 13%);
|
||||||
|
--table-header-bg: hsl(200, 7%, 28%);
|
||||||
|
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #aaa;
|
||||||
|
--searchbar-bg: #b7b7b7;
|
||||||
|
--searchbar-fg: #000;
|
||||||
|
--searchbar-shadow-color: #aaa;
|
||||||
|
--searchresults-header-fg: #666;
|
||||||
|
--searchresults-border-color: #98a3ad;
|
||||||
|
--searchresults-li-bg: #2b2b2f;
|
||||||
|
--search-mark-bg: #355c7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light {
|
||||||
|
--bg: hsl(0, 0%, 100%);
|
||||||
|
--fg: hsl(0, 0%, 0%);
|
||||||
|
|
||||||
|
--sidebar-bg: #fafafa;
|
||||||
|
--sidebar-fg: hsl(0, 0%, 0%);
|
||||||
|
--sidebar-non-existant: #aaaaaa;
|
||||||
|
--sidebar-active: #1f1fff;
|
||||||
|
--sidebar-spacer: #f4f4f4;
|
||||||
|
|
||||||
|
--scrollbar: #8F8F8F;
|
||||||
|
|
||||||
|
--icons: #747474;
|
||||||
|
--icons-hover: #000000;
|
||||||
|
|
||||||
|
--links: #20609f;
|
||||||
|
|
||||||
|
--inline-code-color: #301900;
|
||||||
|
|
||||||
|
--theme-popup-bg: #fafafa;
|
||||||
|
--theme-popup-border: #cccccc;
|
||||||
|
--theme-hover: #e6e6e6;
|
||||||
|
|
||||||
|
--quote-bg: hsl(197, 37%, 96%);
|
||||||
|
--quote-border: hsl(197, 37%, 91%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(0, 0%, 95%);
|
||||||
|
--table-header-bg: hsl(0, 0%, 80%);
|
||||||
|
--table-alternate-bg: hsl(0, 0%, 97%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #aaa;
|
||||||
|
--searchbar-bg: #fafafa;
|
||||||
|
--searchbar-fg: #000;
|
||||||
|
--searchbar-shadow-color: #aaa;
|
||||||
|
--searchresults-header-fg: #666;
|
||||||
|
--searchresults-border-color: #888;
|
||||||
|
--searchresults-li-bg: #e4f2fe;
|
||||||
|
--search-mark-bg: #a2cff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navy {
|
||||||
|
--bg: hsl(226, 23%, 11%);
|
||||||
|
--fg: #bcbdd0;
|
||||||
|
|
||||||
|
--sidebar-bg: #282d3f;
|
||||||
|
--sidebar-fg: #c8c9db;
|
||||||
|
--sidebar-non-existant: #505274;
|
||||||
|
--sidebar-active: #2b79a2;
|
||||||
|
--sidebar-spacer: #2d334f;
|
||||||
|
|
||||||
|
--scrollbar: var(--sidebar-fg);
|
||||||
|
|
||||||
|
--icons: #737480;
|
||||||
|
--icons-hover: #b7b9cc;
|
||||||
|
|
||||||
|
--links: #2b79a2;
|
||||||
|
|
||||||
|
--inline-code-color: #c5c8c6;;
|
||||||
|
|
||||||
|
--theme-popup-bg: #161923;
|
||||||
|
--theme-popup-border: #737480;
|
||||||
|
--theme-hover: #282e40;
|
||||||
|
|
||||||
|
--quote-bg: hsl(226, 15%, 17%);
|
||||||
|
--quote-border: hsl(226, 15%, 22%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(226, 23%, 16%);
|
||||||
|
--table-header-bg: hsl(226, 23%, 31%);
|
||||||
|
--table-alternate-bg: hsl(226, 23%, 14%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #aaa;
|
||||||
|
--searchbar-bg: #aeaec6;
|
||||||
|
--searchbar-fg: #000;
|
||||||
|
--searchbar-shadow-color: #aaa;
|
||||||
|
--searchresults-header-fg: #5f5f71;
|
||||||
|
--searchresults-border-color: #5c5c68;
|
||||||
|
--searchresults-li-bg: #242430;
|
||||||
|
--search-mark-bg: #a2cff5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rust {
|
||||||
|
--bg: hsl(60, 9%, 87%);
|
||||||
|
--fg: #262625;
|
||||||
|
|
||||||
|
--sidebar-bg: #3b2e2a;
|
||||||
|
--sidebar-fg: #c8c9db;
|
||||||
|
--sidebar-non-existant: #505254;
|
||||||
|
--sidebar-active: #e69f67;
|
||||||
|
--sidebar-spacer: #45373a;
|
||||||
|
|
||||||
|
--scrollbar: var(--sidebar-fg);
|
||||||
|
|
||||||
|
--icons: #737480;
|
||||||
|
--icons-hover: #262625;
|
||||||
|
|
||||||
|
--links: #2b79a2;
|
||||||
|
|
||||||
|
--inline-code-color: #6e6b5e;
|
||||||
|
|
||||||
|
--theme-popup-bg: #e1e1db;
|
||||||
|
--theme-popup-border: #b38f6b;
|
||||||
|
--theme-hover: #99908a;
|
||||||
|
|
||||||
|
--quote-bg: hsl(60, 5%, 75%);
|
||||||
|
--quote-border: hsl(60, 5%, 70%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(60, 9%, 82%);
|
||||||
|
--table-header-bg: #b3a497;
|
||||||
|
--table-alternate-bg: hsl(60, 9%, 84%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #aaa;
|
||||||
|
--searchbar-bg: #fafafa;
|
||||||
|
--searchbar-fg: #000;
|
||||||
|
--searchbar-shadow-color: #aaa;
|
||||||
|
--searchresults-header-fg: #666;
|
||||||
|
--searchresults-border-color: #888;
|
||||||
|
--searchresults-li-bg: #dec2a2;
|
||||||
|
--search-mark-bg: #e69f67;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.light.no-js {
|
||||||
|
--bg: hsl(200, 7%, 8%);
|
||||||
|
--fg: #98a3ad;
|
||||||
|
|
||||||
|
--sidebar-bg: #292c2f;
|
||||||
|
--sidebar-fg: #a1adb8;
|
||||||
|
--sidebar-non-existant: #505254;
|
||||||
|
--sidebar-active: #3473ad;
|
||||||
|
--sidebar-spacer: #393939;
|
||||||
|
|
||||||
|
--scrollbar: var(--sidebar-fg);
|
||||||
|
|
||||||
|
--icons: #43484d;
|
||||||
|
--icons-hover: #b3c0cc;
|
||||||
|
|
||||||
|
--links: #2b79a2;
|
||||||
|
|
||||||
|
--inline-code-color: #c5c8c6;;
|
||||||
|
|
||||||
|
--theme-popup-bg: #141617;
|
||||||
|
--theme-popup-border: #43484d;
|
||||||
|
--theme-hover: #1f2124;
|
||||||
|
|
||||||
|
--quote-bg: hsl(234, 21%, 18%);
|
||||||
|
--quote-border: hsl(234, 21%, 23%);
|
||||||
|
|
||||||
|
--table-border-color: hsl(200, 7%, 13%);
|
||||||
|
--table-header-bg: hsl(200, 7%, 28%);
|
||||||
|
--table-alternate-bg: hsl(200, 7%, 11%);
|
||||||
|
|
||||||
|
--searchbar-border-color: #aaa;
|
||||||
|
--searchbar-bg: #b7b7b7;
|
||||||
|
--searchbar-fg: #000;
|
||||||
|
--searchbar-shadow-color: #aaa;
|
||||||
|
--searchresults-header-fg: #666;
|
||||||
|
--searchresults-border-color: #98a3ad;
|
||||||
|
--searchresults-li-bg: #2b2b2f;
|
||||||
|
--search-mark-bg: #355c7d;
|
||||||
|
}
|
||||||
|
}
|
200
taskchampion/data-model.html
Normal file
200
taskchampion/data-model.html
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Data Model - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html" class="active"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="data-model"><a class="header" href="#data-model">Data Model</a></h1>
|
||||||
|
<p>A client manages a single offline instance of a single user's task list, called a replica.
|
||||||
|
This section covers the structure of that data.
|
||||||
|
Note that this data model is visible only on the client; the server does not have access to client data.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="internals.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="storage.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="internals.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="storage.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
10
taskchampion/elasticlunr.min.js
vendored
Normal file
10
taskchampion/elasticlunr.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
taskchampion/favicon.png
Normal file
BIN
taskchampion/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
22
taskchampion/favicon.svg
Normal file
22
taskchampion/favicon.svg
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 199.7 184.2">
|
||||||
|
<style>
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
svg { fill: white; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="M189.5,36.8c0.2,2.8,0,5.1-0.6,6.8L153,162c-0.6,2.1-2,3.7-4.2,5c-2.2,1.2-4.4,1.9-6.7,1.9H31.4c-9.6,0-15.3-2.8-17.3-8.4
|
||||||
|
c-0.8-2.2-0.8-3.9,0.1-5.2c0.9-1.2,2.4-1.8,4.6-1.8H123c7.4,0,12.6-1.4,15.4-4.1s5.7-8.9,8.6-18.4l32.9-108.6
|
||||||
|
c1.8-5.9,1-11.1-2.2-15.6S169.9,0,164,0H72.7c-1,0-3.1,0.4-6.1,1.1l0.1-0.4C64.5,0.2,62.6,0,61,0.1s-3,0.5-4.3,1.4
|
||||||
|
c-1.3,0.9-2.4,1.8-3.2,2.8S52,6.5,51.2,8.1c-0.8,1.6-1.4,3-1.9,4.3s-1.1,2.7-1.8,4.2c-0.7,1.5-1.3,2.7-2,3.7c-0.5,0.6-1.2,1.5-2,2.5
|
||||||
|
s-1.6,2-2.2,2.8s-0.9,1.5-1.1,2.2c-0.2,0.7-0.1,1.8,0.2,3.2c0.3,1.4,0.4,2.4,0.4,3.1c-0.3,3-1.4,6.9-3.3,11.6
|
||||||
|
c-1.9,4.7-3.6,8.1-5.1,10.1c-0.3,0.4-1.2,1.3-2.6,2.7c-1.4,1.4-2.3,2.6-2.6,3.7c-0.3,0.4-0.3,1.5-0.1,3.4c0.3,1.8,0.4,3.1,0.3,3.8
|
||||||
|
c-0.3,2.7-1.3,6.3-3,10.8c-1.7,4.5-3.4,8.2-5,11c-0.2,0.5-0.9,1.4-2,2.8c-1.1,1.4-1.8,2.5-2,3.4c-0.2,0.6-0.1,1.8,0.1,3.4
|
||||||
|
c0.2,1.6,0.2,2.8-0.1,3.6c-0.6,3-1.8,6.7-3.6,11c-1.8,4.3-3.6,7.9-5.4,11c-0.5,0.8-1.1,1.7-2,2.8c-0.8,1.1-1.5,2-2,2.8
|
||||||
|
s-0.8,1.6-1,2.5c-0.1,0.5,0,1.3,0.4,2.3c0.3,1.1,0.4,1.9,0.4,2.6c-0.1,1.1-0.2,2.6-0.5,4.4c-0.2,1.8-0.4,2.9-0.4,3.2
|
||||||
|
c-1.8,4.8-1.7,9.9,0.2,15.2c2.2,6.2,6.2,11.5,11.9,15.8c5.7,4.3,11.7,6.4,17.8,6.4h110.7c5.2,0,10.1-1.7,14.7-5.2s7.7-7.8,9.2-12.9
|
||||||
|
l33-108.6c1.8-5.8,1-10.9-2.2-15.5C194.9,39.7,192.6,38,189.5,36.8z M59.6,122.8L73.8,80c0,0,7,0,10.8,0s28.8-1.7,25.4,17.5
|
||||||
|
c-3.4,19.2-18.8,25.2-36.8,25.4S59.6,122.8,59.6,122.8z M78.6,116.8c4.7-0.1,18.9-2.9,22.1-17.1S89.2,86.3,89.2,86.3l-8.9,0
|
||||||
|
l-10.2,30.5C70.2,116.9,74,116.9,78.6,116.8z M75.3,68.7L89,26.2h9.8l0.8,34l23.6-34h9.9l-13.6,42.5h-7.1l12.5-35.4l-24.5,35.4h-6.8
|
||||||
|
l-0.8-35L82,68.7H75.3z"/>
|
||||||
|
</svg>
|
||||||
|
<!-- Original image Copyright Dave Gandy — CC BY 4.0 License -->
|
After Width: | Height: | Size: 1.8 KiB |
202
taskchampion/fonts/OPEN-SANS-LICENSE.txt
Normal file
202
taskchampion/fonts/OPEN-SANS-LICENSE.txt
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
93
taskchampion/fonts/SOURCE-CODE-PRO-LICENSE.txt
Normal file
93
taskchampion/fonts/SOURCE-CODE-PRO-LICENSE.txt
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
100
taskchampion/fonts/fonts.css
Normal file
100
taskchampion/fonts/fonts.css
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
/* Open Sans is licensed under the Apache License, Version 2.0. See http://www.apache.org/licenses/LICENSE-2.0 */
|
||||||
|
/* Source Code Pro is under the Open Font License. See https://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL */
|
||||||
|
|
||||||
|
/* open-sans-300 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Open Sans Light'), local('OpenSans-Light'),
|
||||||
|
url('open-sans-v17-all-charsets-300.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-300italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 300;
|
||||||
|
src: local('Open Sans Light Italic'), local('OpenSans-LightItalic'),
|
||||||
|
url('open-sans-v17-all-charsets-300italic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-regular - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Regular'), local('OpenSans-Regular'),
|
||||||
|
url('open-sans-v17-all-charsets-regular.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Open Sans Italic'), local('OpenSans-Italic'),
|
||||||
|
url('open-sans-v17-all-charsets-italic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-600 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
src: local('Open Sans SemiBold'), local('OpenSans-SemiBold'),
|
||||||
|
url('open-sans-v17-all-charsets-600.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-600italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
src: local('Open Sans SemiBold Italic'), local('OpenSans-SemiBoldItalic'),
|
||||||
|
url('open-sans-v17-all-charsets-600italic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-700 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold'), local('OpenSans-Bold'),
|
||||||
|
url('open-sans-v17-all-charsets-700.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-700italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Open Sans Bold Italic'), local('OpenSans-BoldItalic'),
|
||||||
|
url('open-sans-v17-all-charsets-700italic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-800 - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
src: local('Open Sans ExtraBold'), local('OpenSans-ExtraBold'),
|
||||||
|
url('open-sans-v17-all-charsets-800.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* open-sans-800italic - latin_vietnamese_latin-ext_greek-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 800;
|
||||||
|
src: local('Open Sans ExtraBold Italic'), local('OpenSans-ExtraBoldItalic'),
|
||||||
|
url('open-sans-v17-all-charsets-800italic.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* source-code-pro-500 - latin_vietnamese_latin-ext_greek_cyrillic-ext_cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Source Code Pro';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
src: url('source-code-pro-v11-all-charsets-500.woff2') format('woff2');
|
||||||
|
}
|
BIN
taskchampion/fonts/open-sans-v17-all-charsets-300.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-300.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-300italic.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-300italic.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-600.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-600.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-600italic.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-600italic.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-700.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-700.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-700italic.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-700italic.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-800.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-800.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-800italic.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-800italic.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-italic.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-italic.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/open-sans-v17-all-charsets-regular.woff2
Normal file
BIN
taskchampion/fonts/open-sans-v17-all-charsets-regular.woff2
Normal file
Binary file not shown.
BIN
taskchampion/fonts/source-code-pro-v11-all-charsets-500.woff2
Normal file
BIN
taskchampion/fonts/source-code-pro-v11-all-charsets-500.woff2
Normal file
Binary file not shown.
83
taskchampion/highlight.css
Normal file
83
taskchampion/highlight.css
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* An increased contrast highlighting scheme loosely based on the
|
||||||
|
* "Base16 Atelier Dune Light" theme by Bram de Haan
|
||||||
|
* (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune)
|
||||||
|
* Original Base16 color scheme by Chris Kempson
|
||||||
|
* (https://github.com/chriskempson/base16)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Comment */
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #575757;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red */
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-selector-class {
|
||||||
|
color: #d70025;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Orange */
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-builtin-name,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-params {
|
||||||
|
color: #b21e00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green */
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-bullet {
|
||||||
|
color: #008200;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blue */
|
||||||
|
.hljs-title,
|
||||||
|
.hljs-section {
|
||||||
|
color: #0030f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Purple */
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-selector-tag {
|
||||||
|
color: #9d00ec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #f6f7f6;
|
||||||
|
color: #000;
|
||||||
|
padding: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
color: #22863a;
|
||||||
|
background-color: #f0fff4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #b31d28;
|
||||||
|
background-color: #ffeef0;
|
||||||
|
}
|
6
taskchampion/highlight.js
Normal file
6
taskchampion/highlight.js
Normal file
File diff suppressed because one or more lines are too long
BIN
taskchampion/images/name_timestamp.png
Normal file
BIN
taskchampion/images/name_timestamp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
192
taskchampion/index.html
Normal file
192
taskchampion/index.html
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Installation - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="installation"><a class="header" href="#installation">Installation</a></h1>
|
||||||
|
<p>As this is currently in development, installation is by cloning the repository and running "cargo build".</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
|
||||||
|
<a rel="next" href="running-sync-server.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
|
||||||
|
<a rel="next" href="running-sync-server.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
192
taskchampion/installation.html
Normal file
192
taskchampion/installation.html
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Installation - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html" class="active"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="installation"><a class="header" href="#installation">Installation</a></h1>
|
||||||
|
<p>As this is currently in development, installation is by cloning the repository and running "cargo build".</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
|
||||||
|
<a rel="next" href="running-sync-server.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
|
||||||
|
<a rel="next" href="running-sync-server.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
200
taskchampion/internals.html
Normal file
200
taskchampion/internals.html
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Internal Details - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html" class="active"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="internal-details"><a class="header" href="#internal-details">Internal Details</a></h1>
|
||||||
|
<p>The following sections get into the details of how TaskChampion works.
|
||||||
|
None of this information is necessary to use TaskChampion, but might be helpful in understanding its behavior.
|
||||||
|
Developers of TaskChampion and of tools that integrate with TaskChampion should be familiar with this information.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="running-sync-server.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="data-model.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="running-sync-server.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="data-model.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
7
taskchampion/mark.min.js
vendored
Normal file
7
taskchampion/mark.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
215
taskchampion/plans.html
Normal file
215
taskchampion/plans.html
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Planned Functionality - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html" class="active"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="planned-functionality"><a class="header" href="#planned-functionality">Planned Functionality</a></h1>
|
||||||
|
<p>This section is a bit of a to-do list for additional functionality to add to the synchronzation system.
|
||||||
|
Each feature has some discussion of how it might be implemented.</p>
|
||||||
|
<h2 id="snapshots"><a class="header" href="#snapshots">Snapshots</a></h2>
|
||||||
|
<p>As designed, storage required on the server would grow with time, as would the time required for new clients to update to the latest version.
|
||||||
|
As an optimization, the server also stores "snapshots" containing a full copy of the task database at a given version.
|
||||||
|
Based on configurable heuristics, it may delete older operations and snapshots, as long as enough data remains for active clients to synchronize and for new clients to initialize.</p>
|
||||||
|
<p>Since snapshots must be computed by clients, the server may "request" a snapshot when providing the latest version to a client.
|
||||||
|
This request comes with a number indicating how much it 'wants" the snapshot.
|
||||||
|
Clients which can easily generate and transmit a snapshot should be generous to the server, while clients with more limited resources can wait until the server's requests are more desperate.
|
||||||
|
The intent is, where possible, to request snapshots created on well-connected desktop clients over mobile and low-power clients.</p>
|
||||||
|
<h2 id="encryption-and-signing"><a class="header" href="#encryption-and-signing">Encryption and Signing</a></h2>
|
||||||
|
<p>From the server's perspective, all data except for version numbers are opaque binary blobs.
|
||||||
|
Clients encrypt and sign these blobs using a symmetric key known only to the clients.
|
||||||
|
This secures the data at-rest on the server.
|
||||||
|
Note that privacy is not complete, as the server still has some information about users, including source and frequency of synchronization transactions and size of those transactions.</p>
|
||||||
|
<h2 id="backups"><a class="header" href="#backups">Backups</a></h2>
|
||||||
|
<p>In this design, the server is little more than an authenticated storage for encrypted blobs provided by the client.
|
||||||
|
To allow for failure or data loss on the server, clients are expected to cache these blobs locally for a short time (a week), along with a server-provided HMAC signature.
|
||||||
|
When data loss is detected -- such as when a client expects the server to have a version N or higher, and the server only has N-1, the client can send those blobs to the server.
|
||||||
|
The server can validate the HMAC and, if successful, add the blobs to its datastore.</p>
|
||||||
|
<h2 id="expiration"><a class="header" href="#expiration">Expiration</a></h2>
|
||||||
|
<p>Deleted tasks remain in the task database, and are simply hidden in most views.
|
||||||
|
All tasks have an expiration time after which they may be flushed, preventing unbounded increase in task database size.
|
||||||
|
However, purging of a task does not satisfy the necessary OT guarantees, so some further formal design work is required before this is implemented.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="sync-protocol.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="sync-protocol.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
701
taskchampion/print.html
Normal file
701
taskchampion/print.html
Normal file
|
@ -0,0 +1,701 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>TaskChampion</title>
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="installation"><a class="header" href="#installation">Installation</a></h1>
|
||||||
|
<p>As this is currently in development, installation is by cloning the repository and running "cargo build".</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="running-the-sync-server"><a class="header" href="#running-the-sync-server">Running the Sync Server</a></h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>NOTE: TaskChampion is still in development and not yet feature-complete.
|
||||||
|
The server is functional, but lacks any administrative features.</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>Run <code>taskchampion-sync-server</code> to start the sync server.
|
||||||
|
Use <code>--port</code> to specify the port it should listen on, and <code>--data-dir</code> to specify the directory which it should store its data.
|
||||||
|
It only serves HTTP; the expectation is that a frontend proxy will be used for HTTPS support.</p>
|
||||||
|
<p>The server has optional parameters <code>--snapshot-days</code> and <code>--snapshot-version</code>, giving the target number of days and versions, respectively, between snapshots of the client state.
|
||||||
|
The default values for these parameters are generally adequate.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="internal-details"><a class="header" href="#internal-details">Internal Details</a></h1>
|
||||||
|
<p>The following sections get into the details of how TaskChampion works.
|
||||||
|
None of this information is necessary to use TaskChampion, but might be helpful in understanding its behavior.
|
||||||
|
Developers of TaskChampion and of tools that integrate with TaskChampion should be familiar with this information.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="data-model"><a class="header" href="#data-model">Data Model</a></h1>
|
||||||
|
<p>A client manages a single offline instance of a single user's task list, called a replica.
|
||||||
|
This section covers the structure of that data.
|
||||||
|
Note that this data model is visible only on the client; the server does not have access to client data.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="replica-storage"><a class="header" href="#replica-storage">Replica Storage</a></h1>
|
||||||
|
<p>Each replica has a storage backend.
|
||||||
|
The interface for this backend is given in <code>crate::taskstorage::Storage</code> and <code>StorageTxn</code>.</p>
|
||||||
|
<p>The storage is transaction-protected, with the expectation of a serializable isolation level.
|
||||||
|
The storage contains the following information:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>tasks</code>: a set of tasks, indexed by UUID</li>
|
||||||
|
<li><code>base_version</code>: the number of the last version sync'd from the server (a single integer)</li>
|
||||||
|
<li><code>operations</code>: all operations performed since base_version</li>
|
||||||
|
<li><code>working_set</code>: a mapping from integer -> UUID, used to keep stable small-integer indexes into the tasks for users' convenience. This data is not synchronized with the server and does not affect any consistency guarantees.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="tasks"><a class="header" href="#tasks">Tasks</a></h2>
|
||||||
|
<p>The tasks are stored as an un-ordered collection, keyed by task UUID.
|
||||||
|
Each task in the database has represented by a key-value map.
|
||||||
|
See <a href="./tasks.html">Tasks</a> for details on the content of that map.</p>
|
||||||
|
<h2 id="operations"><a class="header" href="#operations">Operations</a></h2>
|
||||||
|
<p>Every change to the task database is captured as an operation.
|
||||||
|
In other words, operations act as deltas between database states.
|
||||||
|
Operations are crucial to synchronization of replicas, described in <a href="./sync-model.html">Synchronization Model</a>.</p>
|
||||||
|
<p>Operations are entirely managed by the replica, and some combinations of operations are described as "invalid" here.
|
||||||
|
A replica must not create invalid operations, but should be resilient to receiving invalid operations during a synchronization operation.</p>
|
||||||
|
<p>Each operation has one of the forms </p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code></li>
|
||||||
|
<li><code>Delete(uuid, oldTask)</code></li>
|
||||||
|
<li><code>Update(uuid, property, oldValue, newValue, timestamp)</code></li>
|
||||||
|
<li><code>UndoPoint()</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>The Create form creates a new task.
|
||||||
|
It is invalid to create a task that already exists.</p>
|
||||||
|
<p>Similarly, the Delete form deletes an existing task.
|
||||||
|
It is invalid to delete a task that does not exist.
|
||||||
|
The <code>oldTask</code> property contains the task data from before it was deleted.</p>
|
||||||
|
<p>The Update form updates the given property of the given task, where the property and values are strings.
|
||||||
|
The <code>oldValue</code> gives the old value of the property (or None to create a new property), while <code>newValue</code> gives the new value (or None to delete a property).
|
||||||
|
It is invalid to update a task that does not exist.
|
||||||
|
The timestamp on updates serves as additional metadata and is used to resolve conflicts.</p>
|
||||||
|
<h3 id="application"><a class="header" href="#application">Application</a></h3>
|
||||||
|
<p>Each operation can be "applied" to a task database in a natural way:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Applying <code>Create</code> creates a new, empty task in the task database.</li>
|
||||||
|
<li>Applying <code>Delete</code> deletes a task, including all of its properties, from the task database.</li>
|
||||||
|
<li>Applying <code>Update</code> modifies the properties of a task.</li>
|
||||||
|
<li>Applying <code>UndoPoint</code> does nothing.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="undo"><a class="header" href="#undo">Undo</a></h3>
|
||||||
|
<p>Each operation also contains enough information to reverse its application:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Undoing <code>Create</code> deletes a task.</li>
|
||||||
|
<li>Undoing <code>Delete</code> creates a task, including all of the properties in <code>oldTask</code>.</li>
|
||||||
|
<li>Undoing <code>Update</code> modifies the properties of a task, reverting to <code>oldValue</code>.</li>
|
||||||
|
<li>Undoing <code>UndoPoint</code> does nothing.</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>UndoPoint</code> operation serves as a marker of points in the operation sequence to which the user might wish to undo.
|
||||||
|
For example, creation of a new task with several properities involves several operations, but is a single step from the user's perspective.
|
||||||
|
An "undo" command reverses operations, removing them from the operations sequence, until it reaches an <code>UndoPoint</code> operation.</p>
|
||||||
|
<h3 id="synchronizing-operations"><a class="header" href="#synchronizing-operations">Synchronizing Operations</a></h3>
|
||||||
|
<p>After operations are synchronized to the server, they can no longer be undone.
|
||||||
|
As such, the <a href="./sync-model.html">synchronization model</a> uses simpler operations.
|
||||||
|
Replica operations are converted to sync operations as follows:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code> -> <code>Create(uuid)</code> (no change)</li>
|
||||||
|
<li><code>Delete(uuid, oldTask)</code> -> <code>Delete(uuid)</code></li>
|
||||||
|
<li><code>Update(uuid, property, oldValue, newValue, timestamp)</code> -> <code>Update(uuid, property, newValue, timestamp)</code></li>
|
||||||
|
<li><code>UndoPoint()</code> -> Ø (dropped from operation sequence)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Once a sequence of operations has been synchronized, there is no need to store those operations on the replica.
|
||||||
|
The current implementation deletes operations at that time.
|
||||||
|
An alternative approach is to keep operations for existing tasks, and provide access to those operations as a "history" of modifications to the task.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="task-database"><a class="header" href="#task-database">Task Database</a></h1>
|
||||||
|
<p>The task database is a layer of abstraction above the replica storage layer, responsible for maintaining some important invariants.
|
||||||
|
While the storage is pluggable, there is only one implementation of the task database.</p>
|
||||||
|
<h2 id="reading-data"><a class="header" href="#reading-data">Reading Data</a></h2>
|
||||||
|
<p>The task database provides read access to the data in the replica's storage through a variety of methods on the struct.
|
||||||
|
Each read operation is executed in a transaction, so data may not be consistent between read operations.
|
||||||
|
In practice, this is not an issue for TaskChampion's purposes.</p>
|
||||||
|
<h2 id="working-set"><a class="header" href="#working-set">Working Set</a></h2>
|
||||||
|
<p>The task database maintains the working set.
|
||||||
|
The working set maps small integers to current tasks, for easy reference by command-line users.
|
||||||
|
This is done in such a way that the task numbers remain stable until the working set is rebuilt, at which point gaps in the numbering, such as for completed tasks, are removed by shifting all higher-numbered tasks downward.</p>
|
||||||
|
<p>The working set is not replicated, and is not considered a part of any consistency guarantees in the task database.</p>
|
||||||
|
<h2 id="modifying-data"><a class="header" href="#modifying-data">Modifying Data</a></h2>
|
||||||
|
<p>Modifications to the data set are made by applying operations.
|
||||||
|
Operations are described in <a href="./storage.html">Replica Storage</a>.</p>
|
||||||
|
<p>Each operation is added to the list of operations in the storage, and simultaneously applied to the tasks in that storage.
|
||||||
|
Operations are checked for validity as they are applied.</p>
|
||||||
|
<h2 id="deletion-and-expiration"><a class="header" href="#deletion-and-expiration">Deletion and Expiration</a></h2>
|
||||||
|
<p>Deletion of a task merely changes the task's status to "deleted", leaving it in the Task database.
|
||||||
|
Actual removal of tasks from the task database takes place as part of <em>expiration</em>, triggered by the user as part of a garbage-collection process.
|
||||||
|
Expiration removes tasks with a <code>modified</code> property more than 180 days in the past, by creating a <code>Delete(uuid)</code> operation.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="tasks-1"><a class="header" href="#tasks-1">Tasks</a></h1>
|
||||||
|
<p>Tasks are stored internally as a key/value map with string keys and values.
|
||||||
|
All fields are optional: the <code>Create</code> operation creates an empty task.
|
||||||
|
Display layers should apply appropriate defaults where necessary.</p>
|
||||||
|
<h2 id="atomicity"><a class="header" href="#atomicity">Atomicity</a></h2>
|
||||||
|
<p>The synchronization process does not support read-modify-write operations.
|
||||||
|
For example, suppose tags are updated by reading a list of tags, adding a tag, and writing the result back.
|
||||||
|
This would be captured as an <code>Update</code> operation containing the amended list of tags.
|
||||||
|
Suppose two such <code>Update</code> operations are made in different replicas and must be reconciled:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag1", "2020-11-23T14:21:22Z")</code></li>
|
||||||
|
<li><code>Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag2", "2020-11-23T15:08:57Z")</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>The result of this reconciliation will be <code>oldtag,newtag2</code>, while the user almost certainly intended <code>oldtag,newtag1,newtag2</code>.</p>
|
||||||
|
<p>The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single <code>Update</code> operation.</p>
|
||||||
|
<h2 id="validity"><a class="header" href="#validity">Validity</a></h2>
|
||||||
|
<p><em>Any</em> key/value map is a valid task.
|
||||||
|
Consumers of task data must make a best effort to interpret any map, even if it contains apparently contradictory information.
|
||||||
|
For example, a task with status "completed" but no "end" key present should be interpreted as completed at an unknown time.</p>
|
||||||
|
<h2 id="representations"><a class="header" href="#representations">Representations</a></h2>
|
||||||
|
<p>Integers are stored in decimal notation.</p>
|
||||||
|
<p>Timestamps are stored as UNIX epoch timestamps, in the form of an integer.</p>
|
||||||
|
<h2 id="keys"><a class="header" href="#keys">Keys</a></h2>
|
||||||
|
<p>The following keys, and key formats, are defined:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>status</code> - one of <code>P</code> for a pending task (the default), <code>C</code> for completed, <code>D</code> for deleted, or <code>R</code> for recurring</li>
|
||||||
|
<li><code>description</code> - the one-line summary of the task</li>
|
||||||
|
<li><code>modified</code> - the time of the last modification of this task</li>
|
||||||
|
<li><code>start</code> - the most recent time at which this task was started (a task with no <code>start</code> key is not active)</li>
|
||||||
|
<li><code>end</code> - if present, the time at which this task was completed or deleted (note that this key may not agree with <code>status</code>: it may be present for a pending task, or absent for a deleted or completed task)</li>
|
||||||
|
<li><code>tag_<tag></code> - indicates this task has tag <code><tag></code> (value is an empty string)</li>
|
||||||
|
<li><code>wait</code> - indicates the time before which this task should be hidden, as it is not actionable</li>
|
||||||
|
<li><code>entry</code> - the time at which the task was created</li>
|
||||||
|
<li><code>annotation_<timestamp></code> - value is an annotation created at the given time</li>
|
||||||
|
</ul>
|
||||||
|
<p>Note that while TaskChampion recognizes "recurring" as a status, it does not implement recurrence directly.</p>
|
||||||
|
<h3 id="udas"><a class="header" href="#udas">UDAs</a></h3>
|
||||||
|
<p>Any unrecognized keys are treated as "user-defined attributes" (UDAs).
|
||||||
|
These attributes can be used to store additional data associated with a task.
|
||||||
|
For example, applications that synchronize tasks with other systems such as calendars or team planning services might store unique identifiers for those systems as UDAs.
|
||||||
|
The application defining a UDA defines the format of the value.</p>
|
||||||
|
<p>UDAs <em>should</em> have a namespaced structure of the form <code><namespace>.<key></code>, where <code><namespace></code> identifies the application defining the UDA.
|
||||||
|
For example, a service named "DevSync" synchronizing tasks from GitHub might use UDAs like <code>devsync.github.issue-id</code>.
|
||||||
|
Note that many existing UDAs for Taskwarrior integrations do not follow this pattern; these are referred to as legacy UDAs.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="synchronization-and-the-sync-server"><a class="header" href="#synchronization-and-the-sync-server">Synchronization and the Sync Server</a></h1>
|
||||||
|
<p>This section covers <em>synchronization</em> of <em>replicas</em> containing the same set of tasks.
|
||||||
|
A replica is can perform all operations locally without connecting to a sync server, then share those operations with other replicas when it connects.
|
||||||
|
Sync is a critical feature of TaskChampion, allowing users to consult and update the same task list on multiple devices, without requiring constant connection.</p>
|
||||||
|
<p>This is a complex topic, and the section is broken into several chapters, beginning at the lower levels of the implementation and working up.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="synchronization-model"><a class="header" href="#synchronization-model">Synchronization Model</a></h1>
|
||||||
|
<p>The <a href="./taskdb.html">task database</a> also implements synchronization.
|
||||||
|
Synchronization occurs between disconnected replicas, mediated by a server.
|
||||||
|
The replicas never communicate directly with one another.
|
||||||
|
The server does not have access to the task data; it sees only opaque blobs of data with a small amount of metadata.</p>
|
||||||
|
<p>The synchronization process is a critical part of the task database's functionality, and it cannot function efficiently without occasional synchronization operations</p>
|
||||||
|
<h2 id="operational-transforms"><a class="header" href="#operational-transforms">Operational Transforms</a></h2>
|
||||||
|
<p>Synchronization is based on <a href="https://en.wikipedia.org/wiki/Operational_transformation">operational transformation</a>.
|
||||||
|
This section will assume some familiarity with the concept.</p>
|
||||||
|
<h2 id="state-and-operations"><a class="header" href="#state-and-operations">State and Operations</a></h2>
|
||||||
|
<p>At a given time, the set of tasks in a replica's storage is the essential "state" of that replica.
|
||||||
|
All modifications to that state occur via operations, as defined in <a href="./storage.html">Replica Storage</a>.
|
||||||
|
We can draw a network, or graph, with the nodes representing states and the edges representing operations.
|
||||||
|
For example:</p>
|
||||||
|
<pre><code class="language-text"> o -- State: {abc-d123: 'get groceries', priority L}
|
||||||
|
|
|
||||||
|
| -- Operation: set abc-d123 priority to H
|
||||||
|
|
|
||||||
|
o -- State: {abc-d123: 'get groceries', priority H}
|
||||||
|
</code></pre>
|
||||||
|
<p>For those familiar with distributed version control systems, a state is analogous to a revision, while an operation is analogous to a commit.</p>
|
||||||
|
<p>Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create.
|
||||||
|
Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on.
|
||||||
|
The synchronization process uses operational transformation to "linearize" those operations.
|
||||||
|
This process is analogous (vaguely) to rebasing a sequence of Git commits.</p>
|
||||||
|
<h3 id="sync-operations"><a class="header" href="#sync-operations">Sync Operations</a></h3>
|
||||||
|
<p>The <a href="./storage.html">Replica Storage</a> model contains additional information in its operations that is not included in operations synchronized to other replicas.
|
||||||
|
In this document, we will be discussing "sync operations" of the form</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code></li>
|
||||||
|
<li><code>Delete(uuid)</code></li>
|
||||||
|
<li><code>Update(uuid, property, value, timestamp)</code></li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="versions"><a class="header" href="#versions">Versions</a></h3>
|
||||||
|
<p>Occasionally, database states are given a name (that takes the form of a UUID).
|
||||||
|
The system as a whole (all replicas) constructs a branch-free sequence of versions and the operations that separate each version from the next.
|
||||||
|
The version with the nil UUID is implicitly the empty database.</p>
|
||||||
|
<p>The server stores the operations to change a state from a "parent" version to a "child" version, and provides that information as needed to replicas.
|
||||||
|
Replicas use this information to update their local task databases, and to generate new versions to send to the server.</p>
|
||||||
|
<p>Replicas generate a new version to transmit local changes to the server.
|
||||||
|
The changes are represented as a sequence of operations with the state resulting from the final operation corresponding to the version.
|
||||||
|
In order to keep the versions in a single sequence, the server will only accept a proposed version from a replica if its parent version matches the latest version on the server.</p>
|
||||||
|
<p>In the non-conflict case (such as with a single replica), then, a replica's synchronization process involves gathering up the operations it has accumulated since its last synchronization; bundling those operations into a version; and sending that version to the server.</p>
|
||||||
|
<h3 id="replica-invariant"><a class="header" href="#replica-invariant">Replica Invariant</a></h3>
|
||||||
|
<p>The replica's <a href="./storage.html">storage</a> contains the current state in <code>tasks</code>, the as-yet un-synchronized operations in <code>operations</code>, and the last version at which synchronization occurred in <code>base_version</code>.</p>
|
||||||
|
<p>The replica's un-synchronized operations are already reflected in its local <code>tasks</code>, so the following invariant holds:</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>Applying <code>operations</code> to the set of tasks at <code>base_version</code> gives a set of tasks identical
|
||||||
|
to <code>tasks</code>.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h3 id="transformation"><a class="header" href="#transformation">Transformation</a></h3>
|
||||||
|
<p>When the latest version on the server contains operations that are not present in the replica, then the states have diverged.
|
||||||
|
For example:</p>
|
||||||
|
<pre><code class="language-text"> o -- version N
|
||||||
|
w|\a
|
||||||
|
o o
|
||||||
|
x| \b
|
||||||
|
o o
|
||||||
|
y| \c
|
||||||
|
o o -- replica's local state
|
||||||
|
z|
|
||||||
|
o -- version N+1
|
||||||
|
</code></pre>
|
||||||
|
<p>(diagram notation: <code>o</code> designates a state, lower-case letters designate operations, and versions are presented as if they were numbered sequentially)</p>
|
||||||
|
<p>In this situation, the replica must "rebase" the local operations onto the latest version from the server and try again.
|
||||||
|
This process is performed using operational transformation (OT).
|
||||||
|
The result of this transformation is a sequence of operations based on the latest version, and a sequence of operations the replica can apply to its local task database to reach the same state
|
||||||
|
Continuing the example above, the resulting operations are shown with <code>'</code>:</p>
|
||||||
|
<pre><code class="language-text"> o -- version N
|
||||||
|
w|\a
|
||||||
|
o o
|
||||||
|
x| \b
|
||||||
|
o o
|
||||||
|
y| \c
|
||||||
|
o o -- replica's intermediate local state
|
||||||
|
z| |w'
|
||||||
|
o-N+1 o
|
||||||
|
a'\ |x'
|
||||||
|
o o
|
||||||
|
b'\ |y'
|
||||||
|
o o
|
||||||
|
c'\|z'
|
||||||
|
o -- version N+2
|
||||||
|
</code></pre>
|
||||||
|
<p>The replica applies w' through z' locally, and sends a' through c' to the server as the operations to generate version N+2.
|
||||||
|
Either path through this graph, a-b-c-w'-x'-y'-z' or a'-b'-c'-w-x-y-z, must generate <em>precisely</em> the same final state at version N+2.
|
||||||
|
Careful selection of the operations and the transformation function ensure this.</p>
|
||||||
|
<p>See the comments in the source code for the details of how this transformation process is implemented.</p>
|
||||||
|
<h2 id="synchronization-process"><a class="header" href="#synchronization-process">Synchronization Process</a></h2>
|
||||||
|
<p>To perform a synchronization, the replica first requests the child version of <code>base_version</code> from the server (GetChildVersion).
|
||||||
|
It applies that version to its local <code>tasks</code>, rebases its local <code>operations</code> as described above, and updates <code>base_version</code>.
|
||||||
|
The replica repeats this process until the server indicates no additional child versions exist.
|
||||||
|
If there are no un-synchronized local operations, the process is complete.</p>
|
||||||
|
<p>Otherwise, the replica creates a new version containing its local operations, giving its <code>base_version</code> as the parent version, and transmits that to the server (AddVersion).
|
||||||
|
In most cases, this will succeed, but if another replica has created a new version in the interim, then the new version will conflict with that other replica's new version and the server will respond with the new expected parent version.
|
||||||
|
In this case, the process repeats.
|
||||||
|
If the server indicates a conflict twice with the same expected base version, that is an indication that the replica has diverged (something serious has gone wrong).</p>
|
||||||
|
<h2 id="servers"><a class="header" href="#servers">Servers</a></h2>
|
||||||
|
<p>A replica depends on periodic synchronization for performant operation.
|
||||||
|
Without synchronization, its list of pending operations would grow indefinitely, and tasks could never be expired.
|
||||||
|
So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically.</p>
|
||||||
|
<p>TaskChampion provides a <code>LocalServer</code> for this purpose.
|
||||||
|
It implements the <code>get_child_version</code> and <code>add_version</code> operations as described, storing data on-disk locally, all within the <code>ta</code> binary.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="snapshots"><a class="header" href="#snapshots">Snapshots</a></h1>
|
||||||
|
<p>The basic synchronization model described in the previous page has a few shortcomings:</p>
|
||||||
|
<ul>
|
||||||
|
<li>servers must store an ever-increasing quantity of versions</li>
|
||||||
|
<li>a new replica must download all versions since the beginning in order to derive the current state</li>
|
||||||
|
</ul>
|
||||||
|
<p>Snapshots allow TaskChampion to avoid both of these issues.
|
||||||
|
A snapshot is a copy of the task database at a specific version.
|
||||||
|
It is created by a replica, encrypted, and stored on the server.
|
||||||
|
A new replica can simply download a recent snapshot and apply any additional versions synchronized since that snapshot was made.
|
||||||
|
Servers can delete and reclaim space used by older versions, as long as newer snapshots are available.</p>
|
||||||
|
<h2 id="snapshot-heuristics"><a class="header" href="#snapshot-heuristics">Snapshot Heuristics</a></h2>
|
||||||
|
<p>A server implementation must answer a few questions:</p>
|
||||||
|
<ul>
|
||||||
|
<li>How often should snapshots be made?</li>
|
||||||
|
<li>When can versions be deleted?</li>
|
||||||
|
<li>When can snapshots be deleted?</li>
|
||||||
|
</ul>
|
||||||
|
<p>A critical invariant is that at least one snapshot must exist for any database that does not have a child of the nil version.
|
||||||
|
This ensures that a new replica can always derive the latest state.</p>
|
||||||
|
<p>Aside from that invariant, the server implementation can vary in its answers to these questions, with the following considerations:</p>
|
||||||
|
<p>Snapshots should be made frequently enough that a new replica can initialize quickly.</p>
|
||||||
|
<p>Existing replicas will fail to synchronize if they request a child version that has been deleted.
|
||||||
|
This failure can cause data loss if the replica had local changes.
|
||||||
|
It's conceivable that replicas may not sync for weeks or months if, for example, they are located on a home computer while the user is on holiday.</p>
|
||||||
|
<h2 id="requesting-new-snapshots"><a class="header" href="#requesting-new-snapshots">Requesting New Snapshots</a></h2>
|
||||||
|
<p>The server requests snapshots from replicas, indicating an urgency for the request.
|
||||||
|
Some replicas, such as those running on PCs or servers, can produce a snapshot even at low urgency.
|
||||||
|
Other replicas, in more restricted environments such as mobile devices, will only produce a snapshot at high urgency.
|
||||||
|
This saves resources in these restricted environments.</p>
|
||||||
|
<p>A snapshot must be made on a replica with no unsynchronized operations.
|
||||||
|
As such, it only makes sense to request a snapshot in response to a successful AddVersion request.</p>
|
||||||
|
<h2 id="handling-deleted-versions"><a class="header" href="#handling-deleted-versions">Handling Deleted Versions</a></h2>
|
||||||
|
<p>When a replica requests a child version, the response must distinguish two cases:</p>
|
||||||
|
<ol>
|
||||||
|
<li>No such child version exists because the replica is up-to-date.</li>
|
||||||
|
<li>No such child version exists because it has been deleted, and the replica must re-initialize itself.</li>
|
||||||
|
</ol>
|
||||||
|
<p>The details of this logic are covered in the <a href="./sync-protocol.html">Server-Replica Protocol</a>.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="server-replica-protocol"><a class="header" href="#server-replica-protocol">Server-Replica Protocol</a></h1>
|
||||||
|
<p>The server-replica protocol is defined abstractly in terms of request/response transactions from the replica to the server.
|
||||||
|
This is made concrete in an HTTP representation.</p>
|
||||||
|
<p>The protocol builds on the model presented in the previous chapter, and in particular on the synchronization process.</p>
|
||||||
|
<h2 id="clients"><a class="header" href="#clients">Clients</a></h2>
|
||||||
|
<p>From the server's perspective, replicas accessing the same task history are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history.</p>
|
||||||
|
<p>Each client is identified and authenticated with a "client key", known only to the server and to the replicas replicating the task history.</p>
|
||||||
|
<h2 id="server"><a class="header" href="#server">Server</a></h2>
|
||||||
|
<p>For each client, the server is responsible for storing the task history, in the form of a branch-free sequence of versions.
|
||||||
|
It also stores the latest snapshot, if any exists.</p>
|
||||||
|
<ul>
|
||||||
|
<li>versions: a set of {versionId: UUID, parentVersionId: UUID, historySegment: bytes}</li>
|
||||||
|
<li>latestVersionId: UUID</li>
|
||||||
|
<li>snapshotVersionId: UUID</li>
|
||||||
|
<li>snapshot: bytes</li>
|
||||||
|
</ul>
|
||||||
|
<p>For each client, it stores a set of versions as well as the latest version ID, defaulting to the nil UUID.
|
||||||
|
Each version has a version ID, a parent version ID, and a history segment (opaque data containing the operations for that version).
|
||||||
|
The server should maintain the following invariants for each client:</p>
|
||||||
|
<ol>
|
||||||
|
<li>latestVersionId is nil or exists in the set of versions.</li>
|
||||||
|
<li>Given versions v1 and v2 for a client, with v1.versionId != v2.versionId and v1.parentVersionId != nil, v1.parentVersionId != v2.parentVersionId.
|
||||||
|
In other words, versions do not branch.</li>
|
||||||
|
<li>If snapshotVersionId is nil, then there is a version with parentVersionId == nil.</li>
|
||||||
|
<li>If snapshotVersionId is not nil, then there is a version with parentVersionId = snapshotVersionId.</li>
|
||||||
|
</ol>
|
||||||
|
<p>Note that versions form a linked list beginning with the latestVersionId stored for the client.
|
||||||
|
This linked list need not continue back to a version with v.parentVersionId = nil.
|
||||||
|
It may end at any point when v.parentVersionId is not found in the set of Versions.
|
||||||
|
This observation allows the server to discard older versions.
|
||||||
|
The third invariant prevents the server from discarding versions if there is no snapshot.
|
||||||
|
The fourth invariant prevents the server from discarding versions newer than the snapshot.</p>
|
||||||
|
<h2 id="data-formats"><a class="header" href="#data-formats">Data Formats</a></h2>
|
||||||
|
<h3 id="encryption"><a class="header" href="#encryption">Encryption</a></h3>
|
||||||
|
<p>The client configuration includes an encryption secret of arbitrary length and a clientId to identify itself.
|
||||||
|
This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots).</p>
|
||||||
|
<h4 id="key-derivation"><a class="header" href="#key-derivation">Key Derivation</a></h4>
|
||||||
|
<p>The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations.
|
||||||
|
The salt is the SHA256 hash of the 16-byte form of the client key.</p>
|
||||||
|
<h4 id="encryption-1"><a class="header" href="#encryption-1">Encryption</a></h4>
|
||||||
|
<p>The client uses <a href="https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html">AEAD</a>, with algorithm CHACHA20_POLY1305.
|
||||||
|
The client should generate a random nonce, noting that AEAD is <em>not secure</em> if a nonce is used repeatedly for the same key.</p>
|
||||||
|
<p>AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations.
|
||||||
|
In this protocol, the AAD is always 17 bytes of the form:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>app_id</code> (byte) - always 1</li>
|
||||||
|
<li><code>version_id</code> (16 bytes) - 16-byte form of the version ID associated with this data
|
||||||
|
<ul>
|
||||||
|
<li>for versions (AddVersion, GetChildVersion), the <em>parent</em> version_id</li>
|
||||||
|
<li>for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>app_id</code> field is for future expansion to handle other, non-task data using this protocol.
|
||||||
|
Including it in the AAD ensures that such data cannot be confused with task data.</p>
|
||||||
|
<p>Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's <code>EVP_AEAD_CTX_seal</code>.</p>
|
||||||
|
<h4 id="representation"><a class="header" href="#representation">Representation</a></h4>
|
||||||
|
<p>The final byte-stream is comprised of the following structure:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>version</code> (byte) - format version (always 1)</li>
|
||||||
|
<li><code>nonce</code> (12 bytes) - encryption nonce</li>
|
||||||
|
<li><code>ciphertext</code> (remaining bytes) - ciphertext from sealing operation</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>version</code> field identifies this data format, and future formats will have a value other than 1 in this position.</p>
|
||||||
|
<h3 id="version"><a class="header" href="#version">Version</a></h3>
|
||||||
|
<p>The decrypted form of a version is a JSON array containing operations in the order they should be applied.
|
||||||
|
Each operation has the form <code>{TYPE: DATA}</code>, for example:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>{"Create":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}</code></li>
|
||||||
|
<li><code>{"Delete":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}</code></li>
|
||||||
|
<li><code>{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":"v","timestamp":"2021-10-11T12:47:07.188090948Z"}}</code></li>
|
||||||
|
<li><code>{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":null,"timestamp":"2021-10-11T12:47:07.188090948Z"}}</code> (to delete a property)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Timestamps are in RFC3339 format with a <code>Z</code> suffix.</p>
|
||||||
|
<h3 id="snapshot"><a class="header" href="#snapshot">Snapshot</a></h3>
|
||||||
|
<p>The decrypted form of a snapshot is a JSON object mapping task IDs to task properties.
|
||||||
|
For example (pretty-printed for clarity):</p>
|
||||||
|
<pre><code class="language-json">{
|
||||||
|
"56e0be07-c61f-494c-a54c-bdcfdd52d2a7": {
|
||||||
|
"description": "a task",
|
||||||
|
"priority": "H"
|
||||||
|
},
|
||||||
|
"4b7ed904-f7b0-4293-8a10-ad452422c7b3": {
|
||||||
|
"description": "another task"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<h2 id="transactions"><a class="header" href="#transactions">Transactions</a></h2>
|
||||||
|
<h3 id="addversion"><a class="header" href="#addversion">AddVersion</a></h3>
|
||||||
|
<p>The AddVersion transaction requests that the server add a new version to the client's task history.
|
||||||
|
The request contains the following;</p>
|
||||||
|
<ul>
|
||||||
|
<li>parent version ID</li>
|
||||||
|
<li>history segment</li>
|
||||||
|
</ul>
|
||||||
|
<p>The server determines whether the new version is acceptable, atomically with respect to other requests for the same client.
|
||||||
|
If it has no versions for the client, it accepts the version.
|
||||||
|
If it already has one or more versions for the client, then it accepts the version only if the given parent version ID matches its stored latest parent ID.</p>
|
||||||
|
<p>If the version is accepted, the server generates a new version ID for it.
|
||||||
|
The version is added to the set of versions for the client, the client's latest version ID is set to the new version ID.
|
||||||
|
The new version ID is returned in the response to the client.
|
||||||
|
The response may also include a request for a snapshot, with associated urgency.</p>
|
||||||
|
<p>If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the latest version ID.
|
||||||
|
The client may then "rebase" its operations and try again.
|
||||||
|
Note that if a client receives two conflict responses with the same parent version ID, it is an indication that the client's version history has diverged from that on the server.</p>
|
||||||
|
<h3 id="getchildversion"><a class="header" href="#getchildversion">GetChildVersion</a></h3>
|
||||||
|
<p>The GetChildVersion transaction is a read-only request for a version.
|
||||||
|
The request consists of a parent version ID.
|
||||||
|
The server searches its set of versions for a version with the given parent ID.
|
||||||
|
If found, it returns the version's</p>
|
||||||
|
<ul>
|
||||||
|
<li>version ID,</li>
|
||||||
|
<li>parent version ID (matching that in the request), and</li>
|
||||||
|
<li>history segment.</li>
|
||||||
|
</ul>
|
||||||
|
<p>The response is either a version (success, <em>not-found</em>, or <em>gone</em>, as determined by the first of the following to apply:</p>
|
||||||
|
<ul>
|
||||||
|
<li>If a version with parentVersionId equal to the requested parentVersionId exists, it is returned.</li>
|
||||||
|
<li>If the requested parentVersionId is the nil UUID ..
|
||||||
|
<ul>
|
||||||
|
<li>..and snapshotVersionId is nil, the response is <em>not-found</em> (the client has no versions).</li>
|
||||||
|
<li>..and snapshotVersionId is not nil, the response is <em>gone</em> (the first version has been deleted).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>If a version with versionId equal to the requested parentVersionId exists, the response is <em>not-found</em> (the client is up-to-date)</li>
|
||||||
|
<li>Otherwise, the response is <em>gone</em> (the requested version has been deleted).</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="addsnapshot"><a class="header" href="#addsnapshot">AddSnapshot</a></h3>
|
||||||
|
<p>The AddSnapshot transaction requests that the server store a new snapshot, generated by the client.
|
||||||
|
The request contains the following:</p>
|
||||||
|
<ul>
|
||||||
|
<li>version ID at which the snapshot was made</li>
|
||||||
|
<li>snapshot data (opaque to the server)</li>
|
||||||
|
</ul>
|
||||||
|
<p>The server should validate that the snapshot is for an existing version and is newer than any existing snapshot.
|
||||||
|
It may also validate that the snapshot is for a "recent" version (e.g., one of the last 5 versions).
|
||||||
|
If a snapshot already exists for the given version, the server may keep or discard the new snapshot but should return a success indication to the client.</p>
|
||||||
|
<p>The server response is empty.</p>
|
||||||
|
<h3 id="getsnapshot"><a class="header" href="#getsnapshot">GetSnapshot</a></h3>
|
||||||
|
<p>The GetSnapshot transaction requests that the server provide the latest snapshot.
|
||||||
|
The response contains the snapshot version ID and the snapshot data, if those exist.</p>
|
||||||
|
<h2 id="http-representation"><a class="header" href="#http-representation">HTTP Representation</a></h2>
|
||||||
|
<p>The transactions above are realized for an HTTP server at <code><origin></code> using the HTTP requests and responses described here.
|
||||||
|
The <code>origin</code> <em>should</em> be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption.</p>
|
||||||
|
<p>The replica identifies itself to the server using a <code>clientKey</code> in the form of a UUID.
|
||||||
|
This value is passed with every request in the <code>X-Client-Id</code> header, in its dashed-hex format.</p>
|
||||||
|
<h3 id="addversion-1"><a class="header" href="#addversion-1">AddVersion</a></h3>
|
||||||
|
<p>The request is a <code>POST</code> to <code><origin>/v1/client/add-version/<parentVersionId></code>.
|
||||||
|
The request body contains the history segment, optionally encoded using any encoding supported by actix-web.
|
||||||
|
The content-type must be <code>application/vnd.taskchampion.history-segment</code>.</p>
|
||||||
|
<p>The success response is a 200 OK with an empty body.
|
||||||
|
The new version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
If included, a snapshot request appears in the <code>X-Snapshot-Request</code> header with value <code>urgency=low</code> or <code>urgency=high</code>.</p>
|
||||||
|
<p>On conflict, the response is a 409 CONFLICT with an empty body.
|
||||||
|
The expected parent version ID appears in the <code>X-Parent-Version-Id</code> header.</p>
|
||||||
|
<p>Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification.</p>
|
||||||
|
<h3 id="getchildversion-1"><a class="header" href="#getchildversion-1">GetChildVersion</a></h3>
|
||||||
|
<p>The request is a <code>GET</code> to <code><origin>/v1/client/get-child-version/<parentVersionId></code>.</p>
|
||||||
|
<p>The response is determined as described above.
|
||||||
|
The <em>not-found</em> response is 404 NOT FOUND.
|
||||||
|
The <em>gone</em> response is 410 GONE.
|
||||||
|
Neither has a response body.</p>
|
||||||
|
<p>On success, the response is a 200 OK.
|
||||||
|
The version's history segment is returned in the response body, with content-type <code>application/vnd.taskchampion.history-segment</code>.
|
||||||
|
The version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
The response body may be encoded, in accordance with any <code>Accept-Encoding</code> header in the request.</p>
|
||||||
|
<p>On failure, a client should treat a 404 NOT FOUND as indicating that it is up-to-date.
|
||||||
|
Clients should treat a 410 GONE as a synchronization error.
|
||||||
|
If the client has pending changes to send to the server, based on a now-removed version, then those changes cannot be reconciled and will be lost.
|
||||||
|
The client should, optionally after consulting the user, download and apply the latest snapshot.</p>
|
||||||
|
<h3 id="addsnapshot-1"><a class="header" href="#addsnapshot-1">AddSnapshot</a></h3>
|
||||||
|
<p>The request is a <code>POST</code> to <code><origin>/v1/client/add-snapshot/<versionId></code>.
|
||||||
|
The request body contains the snapshot data, optionally encoded using any encoding supported by actix-web.
|
||||||
|
The content-type must be <code>application/vnd.taskchampion.snapshot</code>.</p>
|
||||||
|
<p>If the version is invalid, as described above, the response should be 400 BAD REQUEST.
|
||||||
|
The server response should be 200 OK on success.</p>
|
||||||
|
<h3 id="getsnapshot-1"><a class="header" href="#getsnapshot-1">GetSnapshot</a></h3>
|
||||||
|
<p>The request is a <code>GET</code> to <code><origin>/v1/client/snapshot</code>.</p>
|
||||||
|
<p>The response is a 200 OK.
|
||||||
|
The snapshot is returned in the response body, with content-type <code>application/vnd.taskchampion.snapshot</code>.
|
||||||
|
The version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
The response body may be encoded, in accordance with any <code>Accept-Encoding</code> header in the request.</p>
|
||||||
|
<p>After downloading and decrypting a snapshot, a client must replace its entire local task database with the content of the snapshot.
|
||||||
|
Any local operations that had not yet been synchronized must be discarded.
|
||||||
|
After the snapshot is applied, the client should begin the synchronization process again, starting from the snapshot version.</p>
|
||||||
|
<div style="break-before: page; page-break-before: always;"></div><h1 id="planned-functionality"><a class="header" href="#planned-functionality">Planned Functionality</a></h1>
|
||||||
|
<p>This section is a bit of a to-do list for additional functionality to add to the synchronzation system.
|
||||||
|
Each feature has some discussion of how it might be implemented.</p>
|
||||||
|
<h2 id="snapshots-1"><a class="header" href="#snapshots-1">Snapshots</a></h2>
|
||||||
|
<p>As designed, storage required on the server would grow with time, as would the time required for new clients to update to the latest version.
|
||||||
|
As an optimization, the server also stores "snapshots" containing a full copy of the task database at a given version.
|
||||||
|
Based on configurable heuristics, it may delete older operations and snapshots, as long as enough data remains for active clients to synchronize and for new clients to initialize.</p>
|
||||||
|
<p>Since snapshots must be computed by clients, the server may "request" a snapshot when providing the latest version to a client.
|
||||||
|
This request comes with a number indicating how much it 'wants" the snapshot.
|
||||||
|
Clients which can easily generate and transmit a snapshot should be generous to the server, while clients with more limited resources can wait until the server's requests are more desperate.
|
||||||
|
The intent is, where possible, to request snapshots created on well-connected desktop clients over mobile and low-power clients.</p>
|
||||||
|
<h2 id="encryption-and-signing"><a class="header" href="#encryption-and-signing">Encryption and Signing</a></h2>
|
||||||
|
<p>From the server's perspective, all data except for version numbers are opaque binary blobs.
|
||||||
|
Clients encrypt and sign these blobs using a symmetric key known only to the clients.
|
||||||
|
This secures the data at-rest on the server.
|
||||||
|
Note that privacy is not complete, as the server still has some information about users, including source and frequency of synchronization transactions and size of those transactions.</p>
|
||||||
|
<h2 id="backups"><a class="header" href="#backups">Backups</a></h2>
|
||||||
|
<p>In this design, the server is little more than an authenticated storage for encrypted blobs provided by the client.
|
||||||
|
To allow for failure or data loss on the server, clients are expected to cache these blobs locally for a short time (a week), along with a server-provided HMAC signature.
|
||||||
|
When data loss is detected -- such as when a client expects the server to have a version N or higher, and the server only has N-1, the client can send those blobs to the server.
|
||||||
|
The server can validate the HMAC and, if successful, add the blobs to its datastore.</p>
|
||||||
|
<h2 id="expiration"><a class="header" href="#expiration">Expiration</a></h2>
|
||||||
|
<p>Deleted tasks remain in the task database, and are simply hidden in most views.
|
||||||
|
All tasks have an expiration time after which they may be flushed, preventing unbounded increase in task database size.
|
||||||
|
However, purging of a task does not satisfy the necessary OT guarantees, so some further formal design work is required before this is implemented.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
window.setTimeout(window.print, 100);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
206
taskchampion/running-sync-server.html
Normal file
206
taskchampion/running-sync-server.html
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Running the Sync Server - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html" class="active"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="running-the-sync-server"><a class="header" href="#running-the-sync-server">Running the Sync Server</a></h1>
|
||||||
|
<blockquote>
|
||||||
|
<p>NOTE: TaskChampion is still in development and not yet feature-complete.
|
||||||
|
The server is functional, but lacks any administrative features.</p>
|
||||||
|
</blockquote>
|
||||||
|
<p>Run <code>taskchampion-sync-server</code> to start the sync server.
|
||||||
|
Use <code>--port</code> to specify the port it should listen on, and <code>--data-dir</code> to specify the directory which it should store its data.
|
||||||
|
It only serves HTTP; the expectation is that a frontend proxy will be used for HTTPS support.</p>
|
||||||
|
<p>The server has optional parameters <code>--snapshot-days</code> and <code>--snapshot-version</code>, giving the target number of days and versions, respectively, between snapshots of the client state.
|
||||||
|
The default values for these parameters are generally adequate.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="installation.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="internals.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="installation.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="internals.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
483
taskchampion/searcher.js
Normal file
483
taskchampion/searcher.js
Normal file
|
@ -0,0 +1,483 @@
|
||||||
|
"use strict";
|
||||||
|
window.search = window.search || {};
|
||||||
|
(function search(search) {
|
||||||
|
// Search functionality
|
||||||
|
//
|
||||||
|
// You can use !hasFocus() to prevent keyhandling in your key
|
||||||
|
// event handlers while the user is typing their search.
|
||||||
|
|
||||||
|
if (!Mark || !elasticlunr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//IE 11 Compatibility from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith
|
||||||
|
if (!String.prototype.startsWith) {
|
||||||
|
String.prototype.startsWith = function(search, pos) {
|
||||||
|
return this.substr(!pos || pos < 0 ? 0 : +pos, search.length) === search;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var search_wrap = document.getElementById('search-wrapper'),
|
||||||
|
searchbar = document.getElementById('searchbar'),
|
||||||
|
searchbar_outer = document.getElementById('searchbar-outer'),
|
||||||
|
searchresults = document.getElementById('searchresults'),
|
||||||
|
searchresults_outer = document.getElementById('searchresults-outer'),
|
||||||
|
searchresults_header = document.getElementById('searchresults-header'),
|
||||||
|
searchicon = document.getElementById('search-toggle'),
|
||||||
|
content = document.getElementById('content'),
|
||||||
|
|
||||||
|
searchindex = null,
|
||||||
|
doc_urls = [],
|
||||||
|
results_options = {
|
||||||
|
teaser_word_count: 30,
|
||||||
|
limit_results: 30,
|
||||||
|
},
|
||||||
|
search_options = {
|
||||||
|
bool: "AND",
|
||||||
|
expand: true,
|
||||||
|
fields: {
|
||||||
|
title: {boost: 1},
|
||||||
|
body: {boost: 1},
|
||||||
|
breadcrumbs: {boost: 0}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mark_exclude = [],
|
||||||
|
marker = new Mark(content),
|
||||||
|
current_searchterm = "",
|
||||||
|
URL_SEARCH_PARAM = 'search',
|
||||||
|
URL_MARK_PARAM = 'highlight',
|
||||||
|
teaser_count = 0,
|
||||||
|
|
||||||
|
SEARCH_HOTKEY_KEYCODE = 83,
|
||||||
|
ESCAPE_KEYCODE = 27,
|
||||||
|
DOWN_KEYCODE = 40,
|
||||||
|
UP_KEYCODE = 38,
|
||||||
|
SELECT_KEYCODE = 13;
|
||||||
|
|
||||||
|
function hasFocus() {
|
||||||
|
return searchbar === document.activeElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeChildren(elem) {
|
||||||
|
while (elem.firstChild) {
|
||||||
|
elem.removeChild(elem.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to parse a url into its building blocks.
|
||||||
|
function parseURL(url) {
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
return {
|
||||||
|
source: url,
|
||||||
|
protocol: a.protocol.replace(':',''),
|
||||||
|
host: a.hostname,
|
||||||
|
port: a.port,
|
||||||
|
params: (function(){
|
||||||
|
var ret = {};
|
||||||
|
var seg = a.search.replace(/^\?/,'').split('&');
|
||||||
|
var len = seg.length, i = 0, s;
|
||||||
|
for (;i<len;i++) {
|
||||||
|
if (!seg[i]) { continue; }
|
||||||
|
s = seg[i].split('=');
|
||||||
|
ret[s[0]] = s[1];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
})(),
|
||||||
|
file: (a.pathname.match(/\/([^/?#]+)$/i) || [,''])[1],
|
||||||
|
hash: a.hash.replace('#',''),
|
||||||
|
path: a.pathname.replace(/^([^/])/,'/$1')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to recreate a url string from its building blocks.
|
||||||
|
function renderURL(urlobject) {
|
||||||
|
var url = urlobject.protocol + "://" + urlobject.host;
|
||||||
|
if (urlobject.port != "") {
|
||||||
|
url += ":" + urlobject.port;
|
||||||
|
}
|
||||||
|
url += urlobject.path;
|
||||||
|
var joiner = "?";
|
||||||
|
for(var prop in urlobject.params) {
|
||||||
|
if(urlobject.params.hasOwnProperty(prop)) {
|
||||||
|
url += joiner + prop + "=" + urlobject.params[prop];
|
||||||
|
joiner = "&";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (urlobject.hash != "") {
|
||||||
|
url += "#" + urlobject.hash;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to escape html special chars for displaying the teasers
|
||||||
|
var escapeHTML = (function() {
|
||||||
|
var MAP = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
var repl = function(c) { return MAP[c]; };
|
||||||
|
return function(s) {
|
||||||
|
return s.replace(/[&<>'"]/g, repl);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
function formatSearchMetric(count, searchterm) {
|
||||||
|
if (count == 1) {
|
||||||
|
return count + " search result for '" + searchterm + "':";
|
||||||
|
} else if (count == 0) {
|
||||||
|
return "No search results for '" + searchterm + "'.";
|
||||||
|
} else {
|
||||||
|
return count + " search results for '" + searchterm + "':";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSearchResult(result, searchterms) {
|
||||||
|
var teaser = makeTeaser(escapeHTML(result.doc.body), searchterms);
|
||||||
|
teaser_count++;
|
||||||
|
|
||||||
|
// The ?URL_MARK_PARAM= parameter belongs inbetween the page and the #heading-anchor
|
||||||
|
var url = doc_urls[result.ref].split("#");
|
||||||
|
if (url.length == 1) { // no anchor found
|
||||||
|
url.push("");
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeURIComponent escapes all chars that could allow an XSS except
|
||||||
|
// for '. Due to that we also manually replace ' with its url-encoded
|
||||||
|
// representation (%27).
|
||||||
|
var searchterms = encodeURIComponent(searchterms.join(" ")).replace(/\'/g, "%27");
|
||||||
|
|
||||||
|
return '<a href="' + path_to_root + url[0] + '?' + URL_MARK_PARAM + '=' + searchterms + '#' + url[1]
|
||||||
|
+ '" aria-details="teaser_' + teaser_count + '">' + result.doc.breadcrumbs + '</a>'
|
||||||
|
+ '<span class="teaser" id="teaser_' + teaser_count + '" aria-label="Search Result Teaser">'
|
||||||
|
+ teaser + '</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeTeaser(body, searchterms) {
|
||||||
|
// The strategy is as follows:
|
||||||
|
// First, assign a value to each word in the document:
|
||||||
|
// Words that correspond to search terms (stemmer aware): 40
|
||||||
|
// Normal words: 2
|
||||||
|
// First word in a sentence: 8
|
||||||
|
// Then use a sliding window with a constant number of words and count the
|
||||||
|
// sum of the values of the words within the window. Then use the window that got the
|
||||||
|
// maximum sum. If there are multiple maximas, then get the last one.
|
||||||
|
// Enclose the terms in <em>.
|
||||||
|
var stemmed_searchterms = searchterms.map(function(w) {
|
||||||
|
return elasticlunr.stemmer(w.toLowerCase());
|
||||||
|
});
|
||||||
|
var searchterm_weight = 40;
|
||||||
|
var weighted = []; // contains elements of ["word", weight, index_in_document]
|
||||||
|
// split in sentences, then words
|
||||||
|
var sentences = body.toLowerCase().split('. ');
|
||||||
|
var index = 0;
|
||||||
|
var value = 0;
|
||||||
|
var searchterm_found = false;
|
||||||
|
for (var sentenceindex in sentences) {
|
||||||
|
var words = sentences[sentenceindex].split(' ');
|
||||||
|
value = 8;
|
||||||
|
for (var wordindex in words) {
|
||||||
|
var word = words[wordindex];
|
||||||
|
if (word.length > 0) {
|
||||||
|
for (var searchtermindex in stemmed_searchterms) {
|
||||||
|
if (elasticlunr.stemmer(word).startsWith(stemmed_searchterms[searchtermindex])) {
|
||||||
|
value = searchterm_weight;
|
||||||
|
searchterm_found = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
weighted.push([word, value, index]);
|
||||||
|
value = 2;
|
||||||
|
}
|
||||||
|
index += word.length;
|
||||||
|
index += 1; // ' ' or '.' if last word in sentence
|
||||||
|
};
|
||||||
|
index += 1; // because we split at a two-char boundary '. '
|
||||||
|
};
|
||||||
|
|
||||||
|
if (weighted.length == 0) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
var window_weight = [];
|
||||||
|
var window_size = Math.min(weighted.length, results_options.teaser_word_count);
|
||||||
|
|
||||||
|
var cur_sum = 0;
|
||||||
|
for (var wordindex = 0; wordindex < window_size; wordindex++) {
|
||||||
|
cur_sum += weighted[wordindex][1];
|
||||||
|
};
|
||||||
|
window_weight.push(cur_sum);
|
||||||
|
for (var wordindex = 0; wordindex < weighted.length - window_size; wordindex++) {
|
||||||
|
cur_sum -= weighted[wordindex][1];
|
||||||
|
cur_sum += weighted[wordindex + window_size][1];
|
||||||
|
window_weight.push(cur_sum);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (searchterm_found) {
|
||||||
|
var max_sum = 0;
|
||||||
|
var max_sum_window_index = 0;
|
||||||
|
// backwards
|
||||||
|
for (var i = window_weight.length - 1; i >= 0; i--) {
|
||||||
|
if (window_weight[i] > max_sum) {
|
||||||
|
max_sum = window_weight[i];
|
||||||
|
max_sum_window_index = i;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
max_sum_window_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add <em/> around searchterms
|
||||||
|
var teaser_split = [];
|
||||||
|
var index = weighted[max_sum_window_index][2];
|
||||||
|
for (var i = max_sum_window_index; i < max_sum_window_index+window_size; i++) {
|
||||||
|
var word = weighted[i];
|
||||||
|
if (index < word[2]) {
|
||||||
|
// missing text from index to start of `word`
|
||||||
|
teaser_split.push(body.substring(index, word[2]));
|
||||||
|
index = word[2];
|
||||||
|
}
|
||||||
|
if (word[1] == searchterm_weight) {
|
||||||
|
teaser_split.push("<em>")
|
||||||
|
}
|
||||||
|
index = word[2] + word[0].length;
|
||||||
|
teaser_split.push(body.substring(word[2], index));
|
||||||
|
if (word[1] == searchterm_weight) {
|
||||||
|
teaser_split.push("</em>")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return teaser_split.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(config) {
|
||||||
|
results_options = config.results_options;
|
||||||
|
search_options = config.search_options;
|
||||||
|
searchbar_outer = config.searchbar_outer;
|
||||||
|
doc_urls = config.doc_urls;
|
||||||
|
searchindex = elasticlunr.Index.load(config.index);
|
||||||
|
|
||||||
|
// Set up events
|
||||||
|
searchicon.addEventListener('click', function(e) { searchIconClickHandler(); }, false);
|
||||||
|
searchbar.addEventListener('keyup', function(e) { searchbarKeyUpHandler(); }, false);
|
||||||
|
document.addEventListener('keydown', function(e) { globalKeyHandler(e); }, false);
|
||||||
|
// If the user uses the browser buttons, do the same as if a reload happened
|
||||||
|
window.onpopstate = function(e) { doSearchOrMarkFromUrl(); };
|
||||||
|
// Suppress "submit" events so the page doesn't reload when the user presses Enter
|
||||||
|
document.addEventListener('submit', function(e) { e.preventDefault(); }, false);
|
||||||
|
|
||||||
|
// If reloaded, do the search or mark again, depending on the current url parameters
|
||||||
|
doSearchOrMarkFromUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unfocusSearchbar() {
|
||||||
|
// hacky, but just focusing a div only works once
|
||||||
|
var tmp = document.createElement('input');
|
||||||
|
tmp.setAttribute('style', 'position: absolute; opacity: 0;');
|
||||||
|
searchicon.appendChild(tmp);
|
||||||
|
tmp.focus();
|
||||||
|
tmp.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On reload or browser history backwards/forwards events, parse the url and do search or mark
|
||||||
|
function doSearchOrMarkFromUrl() {
|
||||||
|
// Check current URL for search request
|
||||||
|
var url = parseURL(window.location.href);
|
||||||
|
if (url.params.hasOwnProperty(URL_SEARCH_PARAM)
|
||||||
|
&& url.params[URL_SEARCH_PARAM] != "") {
|
||||||
|
showSearch(true);
|
||||||
|
searchbar.value = decodeURIComponent(
|
||||||
|
(url.params[URL_SEARCH_PARAM]+'').replace(/\+/g, '%20'));
|
||||||
|
searchbarKeyUpHandler(); // -> doSearch()
|
||||||
|
} else {
|
||||||
|
showSearch(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.params.hasOwnProperty(URL_MARK_PARAM)) {
|
||||||
|
var words = decodeURIComponent(url.params[URL_MARK_PARAM]).split(' ');
|
||||||
|
marker.mark(words, {
|
||||||
|
exclude: mark_exclude
|
||||||
|
});
|
||||||
|
|
||||||
|
var markers = document.querySelectorAll("mark");
|
||||||
|
function hide() {
|
||||||
|
for (var i = 0; i < markers.length; i++) {
|
||||||
|
markers[i].classList.add("fade-out");
|
||||||
|
window.setTimeout(function(e) { marker.unmark(); }, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (var i = 0; i < markers.length; i++) {
|
||||||
|
markers[i].addEventListener('click', hide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for keyevents on `document`
|
||||||
|
function globalKeyHandler(e) {
|
||||||
|
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.target.type === 'textarea' || e.target.type === 'text') { return; }
|
||||||
|
|
||||||
|
if (e.keyCode === ESCAPE_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
searchbar.classList.remove("active");
|
||||||
|
setSearchUrlParameters("",
|
||||||
|
(searchbar.value.trim() !== "") ? "push" : "replace");
|
||||||
|
if (hasFocus()) {
|
||||||
|
unfocusSearchbar();
|
||||||
|
}
|
||||||
|
showSearch(false);
|
||||||
|
marker.unmark();
|
||||||
|
} else if (!hasFocus() && e.keyCode === SEARCH_HOTKEY_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
showSearch(true);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
searchbar.select();
|
||||||
|
} else if (hasFocus() && e.keyCode === DOWN_KEYCODE) {
|
||||||
|
e.preventDefault();
|
||||||
|
unfocusSearchbar();
|
||||||
|
searchresults.firstElementChild.classList.add("focus");
|
||||||
|
} else if (!hasFocus() && (e.keyCode === DOWN_KEYCODE
|
||||||
|
|| e.keyCode === UP_KEYCODE
|
||||||
|
|| e.keyCode === SELECT_KEYCODE)) {
|
||||||
|
// not `:focus` because browser does annoying scrolling
|
||||||
|
var focused = searchresults.querySelector("li.focus");
|
||||||
|
if (!focused) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.keyCode === DOWN_KEYCODE) {
|
||||||
|
var next = focused.nextElementSibling;
|
||||||
|
if (next) {
|
||||||
|
focused.classList.remove("focus");
|
||||||
|
next.classList.add("focus");
|
||||||
|
}
|
||||||
|
} else if (e.keyCode === UP_KEYCODE) {
|
||||||
|
focused.classList.remove("focus");
|
||||||
|
var prev = focused.previousElementSibling;
|
||||||
|
if (prev) {
|
||||||
|
prev.classList.add("focus");
|
||||||
|
} else {
|
||||||
|
searchbar.select();
|
||||||
|
}
|
||||||
|
} else { // SELECT_KEYCODE
|
||||||
|
window.location.assign(focused.querySelector('a'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSearch(yes) {
|
||||||
|
if (yes) {
|
||||||
|
search_wrap.classList.remove('hidden');
|
||||||
|
searchicon.setAttribute('aria-expanded', 'true');
|
||||||
|
} else {
|
||||||
|
search_wrap.classList.add('hidden');
|
||||||
|
searchicon.setAttribute('aria-expanded', 'false');
|
||||||
|
var results = searchresults.children;
|
||||||
|
for (var i = 0; i < results.length; i++) {
|
||||||
|
results[i].classList.remove("focus");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResults(yes) {
|
||||||
|
if (yes) {
|
||||||
|
searchresults_outer.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
searchresults_outer.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for search icon
|
||||||
|
function searchIconClickHandler() {
|
||||||
|
if (search_wrap.classList.contains('hidden')) {
|
||||||
|
showSearch(true);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
searchbar.select();
|
||||||
|
} else {
|
||||||
|
showSearch(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventhandler for keyevents while the searchbar is focused
|
||||||
|
function searchbarKeyUpHandler() {
|
||||||
|
var searchterm = searchbar.value.trim();
|
||||||
|
if (searchterm != "") {
|
||||||
|
searchbar.classList.add("active");
|
||||||
|
doSearch(searchterm);
|
||||||
|
} else {
|
||||||
|
searchbar.classList.remove("active");
|
||||||
|
showResults(false);
|
||||||
|
removeChildren(searchresults);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchUrlParameters(searchterm, "push_if_new_search_else_replace");
|
||||||
|
|
||||||
|
// Remove marks
|
||||||
|
marker.unmark();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update current url with ?URL_SEARCH_PARAM= parameter, remove ?URL_MARK_PARAM and #heading-anchor .
|
||||||
|
// `action` can be one of "push", "replace", "push_if_new_search_else_replace"
|
||||||
|
// and replaces or pushes a new browser history item.
|
||||||
|
// "push_if_new_search_else_replace" pushes if there is no `?URL_SEARCH_PARAM=abc` yet.
|
||||||
|
function setSearchUrlParameters(searchterm, action) {
|
||||||
|
var url = parseURL(window.location.href);
|
||||||
|
var first_search = ! url.params.hasOwnProperty(URL_SEARCH_PARAM);
|
||||||
|
if (searchterm != "" || action == "push_if_new_search_else_replace") {
|
||||||
|
url.params[URL_SEARCH_PARAM] = searchterm;
|
||||||
|
delete url.params[URL_MARK_PARAM];
|
||||||
|
url.hash = "";
|
||||||
|
} else {
|
||||||
|
delete url.params[URL_MARK_PARAM];
|
||||||
|
delete url.params[URL_SEARCH_PARAM];
|
||||||
|
}
|
||||||
|
// A new search will also add a new history item, so the user can go back
|
||||||
|
// to the page prior to searching. A updated search term will only replace
|
||||||
|
// the url.
|
||||||
|
if (action == "push" || (action == "push_if_new_search_else_replace" && first_search) ) {
|
||||||
|
history.pushState({}, document.title, renderURL(url));
|
||||||
|
} else if (action == "replace" || (action == "push_if_new_search_else_replace" && !first_search) ) {
|
||||||
|
history.replaceState({}, document.title, renderURL(url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doSearch(searchterm) {
|
||||||
|
|
||||||
|
// Don't search the same twice
|
||||||
|
if (current_searchterm == searchterm) { return; }
|
||||||
|
else { current_searchterm = searchterm; }
|
||||||
|
|
||||||
|
if (searchindex == null) { return; }
|
||||||
|
|
||||||
|
// Do the actual search
|
||||||
|
var results = searchindex.search(searchterm, search_options);
|
||||||
|
var resultcount = Math.min(results.length, results_options.limit_results);
|
||||||
|
|
||||||
|
// Display search metrics
|
||||||
|
searchresults_header.innerText = formatSearchMetric(resultcount, searchterm);
|
||||||
|
|
||||||
|
// Clear and insert results
|
||||||
|
var searchterms = searchterm.split(' ');
|
||||||
|
removeChildren(searchresults);
|
||||||
|
for(var i = 0; i < resultcount ; i++){
|
||||||
|
var resultElem = document.createElement('li');
|
||||||
|
resultElem.innerHTML = formatSearchResult(results[i], searchterms);
|
||||||
|
searchresults.appendChild(resultElem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display results
|
||||||
|
showResults(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(path_to_root + 'searchindex.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(json => init(json))
|
||||||
|
.catch(error => { // Try to load searchindex.js if fetch failed
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = path_to_root + 'searchindex.js';
|
||||||
|
script.onload = () => init(window.search);
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Exported functions
|
||||||
|
search.hasFocus = hasFocus;
|
||||||
|
})(window.search);
|
1
taskchampion/searchindex.js
Normal file
1
taskchampion/searchindex.js
Normal file
File diff suppressed because one or more lines are too long
1
taskchampion/searchindex.json
Normal file
1
taskchampion/searchindex.json
Normal file
File diff suppressed because one or more lines are too long
235
taskchampion/snapshots.html
Normal file
235
taskchampion/snapshots.html
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Snapshots - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html" class="active"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="snapshots"><a class="header" href="#snapshots">Snapshots</a></h1>
|
||||||
|
<p>The basic synchronization model described in the previous page has a few shortcomings:</p>
|
||||||
|
<ul>
|
||||||
|
<li>servers must store an ever-increasing quantity of versions</li>
|
||||||
|
<li>a new replica must download all versions since the beginning in order to derive the current state</li>
|
||||||
|
</ul>
|
||||||
|
<p>Snapshots allow TaskChampion to avoid both of these issues.
|
||||||
|
A snapshot is a copy of the task database at a specific version.
|
||||||
|
It is created by a replica, encrypted, and stored on the server.
|
||||||
|
A new replica can simply download a recent snapshot and apply any additional versions synchronized since that snapshot was made.
|
||||||
|
Servers can delete and reclaim space used by older versions, as long as newer snapshots are available.</p>
|
||||||
|
<h2 id="snapshot-heuristics"><a class="header" href="#snapshot-heuristics">Snapshot Heuristics</a></h2>
|
||||||
|
<p>A server implementation must answer a few questions:</p>
|
||||||
|
<ul>
|
||||||
|
<li>How often should snapshots be made?</li>
|
||||||
|
<li>When can versions be deleted?</li>
|
||||||
|
<li>When can snapshots be deleted?</li>
|
||||||
|
</ul>
|
||||||
|
<p>A critical invariant is that at least one snapshot must exist for any database that does not have a child of the nil version.
|
||||||
|
This ensures that a new replica can always derive the latest state.</p>
|
||||||
|
<p>Aside from that invariant, the server implementation can vary in its answers to these questions, with the following considerations:</p>
|
||||||
|
<p>Snapshots should be made frequently enough that a new replica can initialize quickly.</p>
|
||||||
|
<p>Existing replicas will fail to synchronize if they request a child version that has been deleted.
|
||||||
|
This failure can cause data loss if the replica had local changes.
|
||||||
|
It's conceivable that replicas may not sync for weeks or months if, for example, they are located on a home computer while the user is on holiday.</p>
|
||||||
|
<h2 id="requesting-new-snapshots"><a class="header" href="#requesting-new-snapshots">Requesting New Snapshots</a></h2>
|
||||||
|
<p>The server requests snapshots from replicas, indicating an urgency for the request.
|
||||||
|
Some replicas, such as those running on PCs or servers, can produce a snapshot even at low urgency.
|
||||||
|
Other replicas, in more restricted environments such as mobile devices, will only produce a snapshot at high urgency.
|
||||||
|
This saves resources in these restricted environments.</p>
|
||||||
|
<p>A snapshot must be made on a replica with no unsynchronized operations.
|
||||||
|
As such, it only makes sense to request a snapshot in response to a successful AddVersion request.</p>
|
||||||
|
<h2 id="handling-deleted-versions"><a class="header" href="#handling-deleted-versions">Handling Deleted Versions</a></h2>
|
||||||
|
<p>When a replica requests a child version, the response must distinguish two cases:</p>
|
||||||
|
<ol>
|
||||||
|
<li>No such child version exists because the replica is up-to-date.</li>
|
||||||
|
<li>No such child version exists because it has been deleted, and the replica must re-initialize itself.</li>
|
||||||
|
</ol>
|
||||||
|
<p>The details of this logic are covered in the <a href="./sync-protocol.html">Server-Replica Protocol</a>.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="sync-model.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync-protocol.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="sync-model.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync-protocol.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
265
taskchampion/storage.html
Normal file
265
taskchampion/storage.html
Normal file
|
@ -0,0 +1,265 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Replica Storage - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html" class="active"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="replica-storage"><a class="header" href="#replica-storage">Replica Storage</a></h1>
|
||||||
|
<p>Each replica has a storage backend.
|
||||||
|
The interface for this backend is given in <code>crate::taskstorage::Storage</code> and <code>StorageTxn</code>.</p>
|
||||||
|
<p>The storage is transaction-protected, with the expectation of a serializable isolation level.
|
||||||
|
The storage contains the following information:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>tasks</code>: a set of tasks, indexed by UUID</li>
|
||||||
|
<li><code>base_version</code>: the number of the last version sync'd from the server (a single integer)</li>
|
||||||
|
<li><code>operations</code>: all operations performed since base_version</li>
|
||||||
|
<li><code>working_set</code>: a mapping from integer -> UUID, used to keep stable small-integer indexes into the tasks for users' convenience. This data is not synchronized with the server and does not affect any consistency guarantees.</li>
|
||||||
|
</ul>
|
||||||
|
<h2 id="tasks"><a class="header" href="#tasks">Tasks</a></h2>
|
||||||
|
<p>The tasks are stored as an un-ordered collection, keyed by task UUID.
|
||||||
|
Each task in the database has represented by a key-value map.
|
||||||
|
See <a href="./tasks.html">Tasks</a> for details on the content of that map.</p>
|
||||||
|
<h2 id="operations"><a class="header" href="#operations">Operations</a></h2>
|
||||||
|
<p>Every change to the task database is captured as an operation.
|
||||||
|
In other words, operations act as deltas between database states.
|
||||||
|
Operations are crucial to synchronization of replicas, described in <a href="./sync-model.html">Synchronization Model</a>.</p>
|
||||||
|
<p>Operations are entirely managed by the replica, and some combinations of operations are described as "invalid" here.
|
||||||
|
A replica must not create invalid operations, but should be resilient to receiving invalid operations during a synchronization operation.</p>
|
||||||
|
<p>Each operation has one of the forms </p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code></li>
|
||||||
|
<li><code>Delete(uuid, oldTask)</code></li>
|
||||||
|
<li><code>Update(uuid, property, oldValue, newValue, timestamp)</code></li>
|
||||||
|
<li><code>UndoPoint()</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>The Create form creates a new task.
|
||||||
|
It is invalid to create a task that already exists.</p>
|
||||||
|
<p>Similarly, the Delete form deletes an existing task.
|
||||||
|
It is invalid to delete a task that does not exist.
|
||||||
|
The <code>oldTask</code> property contains the task data from before it was deleted.</p>
|
||||||
|
<p>The Update form updates the given property of the given task, where the property and values are strings.
|
||||||
|
The <code>oldValue</code> gives the old value of the property (or None to create a new property), while <code>newValue</code> gives the new value (or None to delete a property).
|
||||||
|
It is invalid to update a task that does not exist.
|
||||||
|
The timestamp on updates serves as additional metadata and is used to resolve conflicts.</p>
|
||||||
|
<h3 id="application"><a class="header" href="#application">Application</a></h3>
|
||||||
|
<p>Each operation can be "applied" to a task database in a natural way:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Applying <code>Create</code> creates a new, empty task in the task database.</li>
|
||||||
|
<li>Applying <code>Delete</code> deletes a task, including all of its properties, from the task database.</li>
|
||||||
|
<li>Applying <code>Update</code> modifies the properties of a task.</li>
|
||||||
|
<li>Applying <code>UndoPoint</code> does nothing.</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="undo"><a class="header" href="#undo">Undo</a></h3>
|
||||||
|
<p>Each operation also contains enough information to reverse its application:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Undoing <code>Create</code> deletes a task.</li>
|
||||||
|
<li>Undoing <code>Delete</code> creates a task, including all of the properties in <code>oldTask</code>.</li>
|
||||||
|
<li>Undoing <code>Update</code> modifies the properties of a task, reverting to <code>oldValue</code>.</li>
|
||||||
|
<li>Undoing <code>UndoPoint</code> does nothing.</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>UndoPoint</code> operation serves as a marker of points in the operation sequence to which the user might wish to undo.
|
||||||
|
For example, creation of a new task with several properities involves several operations, but is a single step from the user's perspective.
|
||||||
|
An "undo" command reverses operations, removing them from the operations sequence, until it reaches an <code>UndoPoint</code> operation.</p>
|
||||||
|
<h3 id="synchronizing-operations"><a class="header" href="#synchronizing-operations">Synchronizing Operations</a></h3>
|
||||||
|
<p>After operations are synchronized to the server, they can no longer be undone.
|
||||||
|
As such, the <a href="./sync-model.html">synchronization model</a> uses simpler operations.
|
||||||
|
Replica operations are converted to sync operations as follows:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code> -> <code>Create(uuid)</code> (no change)</li>
|
||||||
|
<li><code>Delete(uuid, oldTask)</code> -> <code>Delete(uuid)</code></li>
|
||||||
|
<li><code>Update(uuid, property, oldValue, newValue, timestamp)</code> -> <code>Update(uuid, property, newValue, timestamp)</code></li>
|
||||||
|
<li><code>UndoPoint()</code> -> Ø (dropped from operation sequence)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Once a sequence of operations has been synchronized, there is no need to store those operations on the replica.
|
||||||
|
The current implementation deletes operations at that time.
|
||||||
|
An alternative approach is to keep operations for existing tasks, and provide access to those operations as a "history" of modifications to the task.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="data-model.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="taskdb.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="data-model.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="taskdb.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
299
taskchampion/sync-model.html
Normal file
299
taskchampion/sync-model.html
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Synchronization Model - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html" class="active"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="synchronization-model"><a class="header" href="#synchronization-model">Synchronization Model</a></h1>
|
||||||
|
<p>The <a href="./taskdb.html">task database</a> also implements synchronization.
|
||||||
|
Synchronization occurs between disconnected replicas, mediated by a server.
|
||||||
|
The replicas never communicate directly with one another.
|
||||||
|
The server does not have access to the task data; it sees only opaque blobs of data with a small amount of metadata.</p>
|
||||||
|
<p>The synchronization process is a critical part of the task database's functionality, and it cannot function efficiently without occasional synchronization operations</p>
|
||||||
|
<h2 id="operational-transforms"><a class="header" href="#operational-transforms">Operational Transforms</a></h2>
|
||||||
|
<p>Synchronization is based on <a href="https://en.wikipedia.org/wiki/Operational_transformation">operational transformation</a>.
|
||||||
|
This section will assume some familiarity with the concept.</p>
|
||||||
|
<h2 id="state-and-operations"><a class="header" href="#state-and-operations">State and Operations</a></h2>
|
||||||
|
<p>At a given time, the set of tasks in a replica's storage is the essential "state" of that replica.
|
||||||
|
All modifications to that state occur via operations, as defined in <a href="./storage.html">Replica Storage</a>.
|
||||||
|
We can draw a network, or graph, with the nodes representing states and the edges representing operations.
|
||||||
|
For example:</p>
|
||||||
|
<pre><code class="language-text"> o -- State: {abc-d123: 'get groceries', priority L}
|
||||||
|
|
|
||||||
|
| -- Operation: set abc-d123 priority to H
|
||||||
|
|
|
||||||
|
o -- State: {abc-d123: 'get groceries', priority H}
|
||||||
|
</code></pre>
|
||||||
|
<p>For those familiar with distributed version control systems, a state is analogous to a revision, while an operation is analogous to a commit.</p>
|
||||||
|
<p>Fundamentally, synchronization involves all replicas agreeing on a single, linear sequence of operations and the state that those operations create.
|
||||||
|
Since the replicas are not connected, each may have additional operations that have been applied locally, but which have not yet been agreed on.
|
||||||
|
The synchronization process uses operational transformation to "linearize" those operations.
|
||||||
|
This process is analogous (vaguely) to rebasing a sequence of Git commits.</p>
|
||||||
|
<h3 id="sync-operations"><a class="header" href="#sync-operations">Sync Operations</a></h3>
|
||||||
|
<p>The <a href="./storage.html">Replica Storage</a> model contains additional information in its operations that is not included in operations synchronized to other replicas.
|
||||||
|
In this document, we will be discussing "sync operations" of the form</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Create(uuid)</code></li>
|
||||||
|
<li><code>Delete(uuid)</code></li>
|
||||||
|
<li><code>Update(uuid, property, value, timestamp)</code></li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="versions"><a class="header" href="#versions">Versions</a></h3>
|
||||||
|
<p>Occasionally, database states are given a name (that takes the form of a UUID).
|
||||||
|
The system as a whole (all replicas) constructs a branch-free sequence of versions and the operations that separate each version from the next.
|
||||||
|
The version with the nil UUID is implicitly the empty database.</p>
|
||||||
|
<p>The server stores the operations to change a state from a "parent" version to a "child" version, and provides that information as needed to replicas.
|
||||||
|
Replicas use this information to update their local task databases, and to generate new versions to send to the server.</p>
|
||||||
|
<p>Replicas generate a new version to transmit local changes to the server.
|
||||||
|
The changes are represented as a sequence of operations with the state resulting from the final operation corresponding to the version.
|
||||||
|
In order to keep the versions in a single sequence, the server will only accept a proposed version from a replica if its parent version matches the latest version on the server.</p>
|
||||||
|
<p>In the non-conflict case (such as with a single replica), then, a replica's synchronization process involves gathering up the operations it has accumulated since its last synchronization; bundling those operations into a version; and sending that version to the server.</p>
|
||||||
|
<h3 id="replica-invariant"><a class="header" href="#replica-invariant">Replica Invariant</a></h3>
|
||||||
|
<p>The replica's <a href="./storage.html">storage</a> contains the current state in <code>tasks</code>, the as-yet un-synchronized operations in <code>operations</code>, and the last version at which synchronization occurred in <code>base_version</code>.</p>
|
||||||
|
<p>The replica's un-synchronized operations are already reflected in its local <code>tasks</code>, so the following invariant holds:</p>
|
||||||
|
<blockquote>
|
||||||
|
<p>Applying <code>operations</code> to the set of tasks at <code>base_version</code> gives a set of tasks identical
|
||||||
|
to <code>tasks</code>.</p>
|
||||||
|
</blockquote>
|
||||||
|
<h3 id="transformation"><a class="header" href="#transformation">Transformation</a></h3>
|
||||||
|
<p>When the latest version on the server contains operations that are not present in the replica, then the states have diverged.
|
||||||
|
For example:</p>
|
||||||
|
<pre><code class="language-text"> o -- version N
|
||||||
|
w|\a
|
||||||
|
o o
|
||||||
|
x| \b
|
||||||
|
o o
|
||||||
|
y| \c
|
||||||
|
o o -- replica's local state
|
||||||
|
z|
|
||||||
|
o -- version N+1
|
||||||
|
</code></pre>
|
||||||
|
<p>(diagram notation: <code>o</code> designates a state, lower-case letters designate operations, and versions are presented as if they were numbered sequentially)</p>
|
||||||
|
<p>In this situation, the replica must "rebase" the local operations onto the latest version from the server and try again.
|
||||||
|
This process is performed using operational transformation (OT).
|
||||||
|
The result of this transformation is a sequence of operations based on the latest version, and a sequence of operations the replica can apply to its local task database to reach the same state
|
||||||
|
Continuing the example above, the resulting operations are shown with <code>'</code>:</p>
|
||||||
|
<pre><code class="language-text"> o -- version N
|
||||||
|
w|\a
|
||||||
|
o o
|
||||||
|
x| \b
|
||||||
|
o o
|
||||||
|
y| \c
|
||||||
|
o o -- replica's intermediate local state
|
||||||
|
z| |w'
|
||||||
|
o-N+1 o
|
||||||
|
a'\ |x'
|
||||||
|
o o
|
||||||
|
b'\ |y'
|
||||||
|
o o
|
||||||
|
c'\|z'
|
||||||
|
o -- version N+2
|
||||||
|
</code></pre>
|
||||||
|
<p>The replica applies w' through z' locally, and sends a' through c' to the server as the operations to generate version N+2.
|
||||||
|
Either path through this graph, a-b-c-w'-x'-y'-z' or a'-b'-c'-w-x-y-z, must generate <em>precisely</em> the same final state at version N+2.
|
||||||
|
Careful selection of the operations and the transformation function ensure this.</p>
|
||||||
|
<p>See the comments in the source code for the details of how this transformation process is implemented.</p>
|
||||||
|
<h2 id="synchronization-process"><a class="header" href="#synchronization-process">Synchronization Process</a></h2>
|
||||||
|
<p>To perform a synchronization, the replica first requests the child version of <code>base_version</code> from the server (GetChildVersion).
|
||||||
|
It applies that version to its local <code>tasks</code>, rebases its local <code>operations</code> as described above, and updates <code>base_version</code>.
|
||||||
|
The replica repeats this process until the server indicates no additional child versions exist.
|
||||||
|
If there are no un-synchronized local operations, the process is complete.</p>
|
||||||
|
<p>Otherwise, the replica creates a new version containing its local operations, giving its <code>base_version</code> as the parent version, and transmits that to the server (AddVersion).
|
||||||
|
In most cases, this will succeed, but if another replica has created a new version in the interim, then the new version will conflict with that other replica's new version and the server will respond with the new expected parent version.
|
||||||
|
In this case, the process repeats.
|
||||||
|
If the server indicates a conflict twice with the same expected base version, that is an indication that the replica has diverged (something serious has gone wrong).</p>
|
||||||
|
<h2 id="servers"><a class="header" href="#servers">Servers</a></h2>
|
||||||
|
<p>A replica depends on periodic synchronization for performant operation.
|
||||||
|
Without synchronization, its list of pending operations would grow indefinitely, and tasks could never be expired.
|
||||||
|
So all replicas, even "singleton" replicas which do not replicate task data with any other replica, must synchronize periodically.</p>
|
||||||
|
<p>TaskChampion provides a <code>LocalServer</code> for this purpose.
|
||||||
|
It implements the <code>get_child_version</code> and <code>add_version</code> operations as described, storing data on-disk locally, all within the <code>ta</code> binary.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="sync.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="snapshots.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="sync.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="snapshots.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
381
taskchampion/sync-protocol.html
Normal file
381
taskchampion/sync-protocol.html
Normal file
|
@ -0,0 +1,381 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Server-Replica Protocol - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html" class="active"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="server-replica-protocol"><a class="header" href="#server-replica-protocol">Server-Replica Protocol</a></h1>
|
||||||
|
<p>The server-replica protocol is defined abstractly in terms of request/response transactions from the replica to the server.
|
||||||
|
This is made concrete in an HTTP representation.</p>
|
||||||
|
<p>The protocol builds on the model presented in the previous chapter, and in particular on the synchronization process.</p>
|
||||||
|
<h2 id="clients"><a class="header" href="#clients">Clients</a></h2>
|
||||||
|
<p>From the server's perspective, replicas accessing the same task history are indistinguishable, so this protocol uses the term "client" to refer generically to all replicas replicating a single task history.</p>
|
||||||
|
<p>Each client is identified and authenticated with a "client key", known only to the server and to the replicas replicating the task history.</p>
|
||||||
|
<h2 id="server"><a class="header" href="#server">Server</a></h2>
|
||||||
|
<p>For each client, the server is responsible for storing the task history, in the form of a branch-free sequence of versions.
|
||||||
|
It also stores the latest snapshot, if any exists.</p>
|
||||||
|
<ul>
|
||||||
|
<li>versions: a set of {versionId: UUID, parentVersionId: UUID, historySegment: bytes}</li>
|
||||||
|
<li>latestVersionId: UUID</li>
|
||||||
|
<li>snapshotVersionId: UUID</li>
|
||||||
|
<li>snapshot: bytes</li>
|
||||||
|
</ul>
|
||||||
|
<p>For each client, it stores a set of versions as well as the latest version ID, defaulting to the nil UUID.
|
||||||
|
Each version has a version ID, a parent version ID, and a history segment (opaque data containing the operations for that version).
|
||||||
|
The server should maintain the following invariants for each client:</p>
|
||||||
|
<ol>
|
||||||
|
<li>latestVersionId is nil or exists in the set of versions.</li>
|
||||||
|
<li>Given versions v1 and v2 for a client, with v1.versionId != v2.versionId and v1.parentVersionId != nil, v1.parentVersionId != v2.parentVersionId.
|
||||||
|
In other words, versions do not branch.</li>
|
||||||
|
<li>If snapshotVersionId is nil, then there is a version with parentVersionId == nil.</li>
|
||||||
|
<li>If snapshotVersionId is not nil, then there is a version with parentVersionId = snapshotVersionId.</li>
|
||||||
|
</ol>
|
||||||
|
<p>Note that versions form a linked list beginning with the latestVersionId stored for the client.
|
||||||
|
This linked list need not continue back to a version with v.parentVersionId = nil.
|
||||||
|
It may end at any point when v.parentVersionId is not found in the set of Versions.
|
||||||
|
This observation allows the server to discard older versions.
|
||||||
|
The third invariant prevents the server from discarding versions if there is no snapshot.
|
||||||
|
The fourth invariant prevents the server from discarding versions newer than the snapshot.</p>
|
||||||
|
<h2 id="data-formats"><a class="header" href="#data-formats">Data Formats</a></h2>
|
||||||
|
<h3 id="encryption"><a class="header" href="#encryption">Encryption</a></h3>
|
||||||
|
<p>The client configuration includes an encryption secret of arbitrary length and a clientId to identify itself.
|
||||||
|
This section describes how that information is used to encrypt and decrypt data sent to the server (versions and snapshots).</p>
|
||||||
|
<h4 id="key-derivation"><a class="header" href="#key-derivation">Key Derivation</a></h4>
|
||||||
|
<p>The client derives the 32-byte encryption key from the configured encryption secret using PBKDF2 with HMAC-SHA256 and 100,000 iterations.
|
||||||
|
The salt is the SHA256 hash of the 16-byte form of the client key.</p>
|
||||||
|
<h4 id="encryption-1"><a class="header" href="#encryption-1">Encryption</a></h4>
|
||||||
|
<p>The client uses <a href="https://commondatastorage.googleapis.com/chromium-boringssl-docs/aead.h.html">AEAD</a>, with algorithm CHACHA20_POLY1305.
|
||||||
|
The client should generate a random nonce, noting that AEAD is <em>not secure</em> if a nonce is used repeatedly for the same key.</p>
|
||||||
|
<p>AEAD supports additional authenticated data (AAD) which must be provided for both open and seal operations.
|
||||||
|
In this protocol, the AAD is always 17 bytes of the form:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>app_id</code> (byte) - always 1</li>
|
||||||
|
<li><code>version_id</code> (16 bytes) - 16-byte form of the version ID associated with this data
|
||||||
|
<ul>
|
||||||
|
<li>for versions (AddVersion, GetChildVersion), the <em>parent</em> version_id</li>
|
||||||
|
<li>for snapshots (AddSnapshot, GetSnapshot), the snapshot version_id</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>app_id</code> field is for future expansion to handle other, non-task data using this protocol.
|
||||||
|
Including it in the AAD ensures that such data cannot be confused with task data.</p>
|
||||||
|
<p>Although the AEAD specification distinguishes ciphertext and tags, for purposes of this specification they are considered concatenated into a single bytestring as in BoringSSL's <code>EVP_AEAD_CTX_seal</code>.</p>
|
||||||
|
<h4 id="representation"><a class="header" href="#representation">Representation</a></h4>
|
||||||
|
<p>The final byte-stream is comprised of the following structure:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>version</code> (byte) - format version (always 1)</li>
|
||||||
|
<li><code>nonce</code> (12 bytes) - encryption nonce</li>
|
||||||
|
<li><code>ciphertext</code> (remaining bytes) - ciphertext from sealing operation</li>
|
||||||
|
</ul>
|
||||||
|
<p>The <code>version</code> field identifies this data format, and future formats will have a value other than 1 in this position.</p>
|
||||||
|
<h3 id="version"><a class="header" href="#version">Version</a></h3>
|
||||||
|
<p>The decrypted form of a version is a JSON array containing operations in the order they should be applied.
|
||||||
|
Each operation has the form <code>{TYPE: DATA}</code>, for example:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>{"Create":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}</code></li>
|
||||||
|
<li><code>{"Delete":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7"}}</code></li>
|
||||||
|
<li><code>{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":"v","timestamp":"2021-10-11T12:47:07.188090948Z"}}</code></li>
|
||||||
|
<li><code>{"Update":{"uuid":"56e0be07-c61f-494c-a54c-bdcfdd52d2a7","property":"prop","value":null,"timestamp":"2021-10-11T12:47:07.188090948Z"}}</code> (to delete a property)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Timestamps are in RFC3339 format with a <code>Z</code> suffix.</p>
|
||||||
|
<h3 id="snapshot"><a class="header" href="#snapshot">Snapshot</a></h3>
|
||||||
|
<p>The decrypted form of a snapshot is a JSON object mapping task IDs to task properties.
|
||||||
|
For example (pretty-printed for clarity):</p>
|
||||||
|
<pre><code class="language-json">{
|
||||||
|
"56e0be07-c61f-494c-a54c-bdcfdd52d2a7": {
|
||||||
|
"description": "a task",
|
||||||
|
"priority": "H"
|
||||||
|
},
|
||||||
|
"4b7ed904-f7b0-4293-8a10-ad452422c7b3": {
|
||||||
|
"description": "another task"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</code></pre>
|
||||||
|
<h2 id="transactions"><a class="header" href="#transactions">Transactions</a></h2>
|
||||||
|
<h3 id="addversion"><a class="header" href="#addversion">AddVersion</a></h3>
|
||||||
|
<p>The AddVersion transaction requests that the server add a new version to the client's task history.
|
||||||
|
The request contains the following;</p>
|
||||||
|
<ul>
|
||||||
|
<li>parent version ID</li>
|
||||||
|
<li>history segment</li>
|
||||||
|
</ul>
|
||||||
|
<p>The server determines whether the new version is acceptable, atomically with respect to other requests for the same client.
|
||||||
|
If it has no versions for the client, it accepts the version.
|
||||||
|
If it already has one or more versions for the client, then it accepts the version only if the given parent version ID matches its stored latest parent ID.</p>
|
||||||
|
<p>If the version is accepted, the server generates a new version ID for it.
|
||||||
|
The version is added to the set of versions for the client, the client's latest version ID is set to the new version ID.
|
||||||
|
The new version ID is returned in the response to the client.
|
||||||
|
The response may also include a request for a snapshot, with associated urgency.</p>
|
||||||
|
<p>If the version is not accepted, the server makes no changes, but responds to the client with a conflict indication containing the latest version ID.
|
||||||
|
The client may then "rebase" its operations and try again.
|
||||||
|
Note that if a client receives two conflict responses with the same parent version ID, it is an indication that the client's version history has diverged from that on the server.</p>
|
||||||
|
<h3 id="getchildversion"><a class="header" href="#getchildversion">GetChildVersion</a></h3>
|
||||||
|
<p>The GetChildVersion transaction is a read-only request for a version.
|
||||||
|
The request consists of a parent version ID.
|
||||||
|
The server searches its set of versions for a version with the given parent ID.
|
||||||
|
If found, it returns the version's</p>
|
||||||
|
<ul>
|
||||||
|
<li>version ID,</li>
|
||||||
|
<li>parent version ID (matching that in the request), and</li>
|
||||||
|
<li>history segment.</li>
|
||||||
|
</ul>
|
||||||
|
<p>The response is either a version (success, <em>not-found</em>, or <em>gone</em>, as determined by the first of the following to apply:</p>
|
||||||
|
<ul>
|
||||||
|
<li>If a version with parentVersionId equal to the requested parentVersionId exists, it is returned.</li>
|
||||||
|
<li>If the requested parentVersionId is the nil UUID ..
|
||||||
|
<ul>
|
||||||
|
<li>..and snapshotVersionId is nil, the response is <em>not-found</em> (the client has no versions).</li>
|
||||||
|
<li>..and snapshotVersionId is not nil, the response is <em>gone</em> (the first version has been deleted).</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>If a version with versionId equal to the requested parentVersionId exists, the response is <em>not-found</em> (the client is up-to-date)</li>
|
||||||
|
<li>Otherwise, the response is <em>gone</em> (the requested version has been deleted).</li>
|
||||||
|
</ul>
|
||||||
|
<h3 id="addsnapshot"><a class="header" href="#addsnapshot">AddSnapshot</a></h3>
|
||||||
|
<p>The AddSnapshot transaction requests that the server store a new snapshot, generated by the client.
|
||||||
|
The request contains the following:</p>
|
||||||
|
<ul>
|
||||||
|
<li>version ID at which the snapshot was made</li>
|
||||||
|
<li>snapshot data (opaque to the server)</li>
|
||||||
|
</ul>
|
||||||
|
<p>The server should validate that the snapshot is for an existing version and is newer than any existing snapshot.
|
||||||
|
It may also validate that the snapshot is for a "recent" version (e.g., one of the last 5 versions).
|
||||||
|
If a snapshot already exists for the given version, the server may keep or discard the new snapshot but should return a success indication to the client.</p>
|
||||||
|
<p>The server response is empty.</p>
|
||||||
|
<h3 id="getsnapshot"><a class="header" href="#getsnapshot">GetSnapshot</a></h3>
|
||||||
|
<p>The GetSnapshot transaction requests that the server provide the latest snapshot.
|
||||||
|
The response contains the snapshot version ID and the snapshot data, if those exist.</p>
|
||||||
|
<h2 id="http-representation"><a class="header" href="#http-representation">HTTP Representation</a></h2>
|
||||||
|
<p>The transactions above are realized for an HTTP server at <code><origin></code> using the HTTP requests and responses described here.
|
||||||
|
The <code>origin</code> <em>should</em> be an HTTPS endpoint on general principle, but nothing in the functonality or security of the protocol depends on connection encryption.</p>
|
||||||
|
<p>The replica identifies itself to the server using a <code>clientKey</code> in the form of a UUID.
|
||||||
|
This value is passed with every request in the <code>X-Client-Id</code> header, in its dashed-hex format.</p>
|
||||||
|
<h3 id="addversion-1"><a class="header" href="#addversion-1">AddVersion</a></h3>
|
||||||
|
<p>The request is a <code>POST</code> to <code><origin>/v1/client/add-version/<parentVersionId></code>.
|
||||||
|
The request body contains the history segment, optionally encoded using any encoding supported by actix-web.
|
||||||
|
The content-type must be <code>application/vnd.taskchampion.history-segment</code>.</p>
|
||||||
|
<p>The success response is a 200 OK with an empty body.
|
||||||
|
The new version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
If included, a snapshot request appears in the <code>X-Snapshot-Request</code> header with value <code>urgency=low</code> or <code>urgency=high</code>.</p>
|
||||||
|
<p>On conflict, the response is a 409 CONFLICT with an empty body.
|
||||||
|
The expected parent version ID appears in the <code>X-Parent-Version-Id</code> header.</p>
|
||||||
|
<p>Other error responses (4xx or 5xx) may be returned and should be treated appropriately to their meanings in the HTTP specification.</p>
|
||||||
|
<h3 id="getchildversion-1"><a class="header" href="#getchildversion-1">GetChildVersion</a></h3>
|
||||||
|
<p>The request is a <code>GET</code> to <code><origin>/v1/client/get-child-version/<parentVersionId></code>.</p>
|
||||||
|
<p>The response is determined as described above.
|
||||||
|
The <em>not-found</em> response is 404 NOT FOUND.
|
||||||
|
The <em>gone</em> response is 410 GONE.
|
||||||
|
Neither has a response body.</p>
|
||||||
|
<p>On success, the response is a 200 OK.
|
||||||
|
The version's history segment is returned in the response body, with content-type <code>application/vnd.taskchampion.history-segment</code>.
|
||||||
|
The version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
The response body may be encoded, in accordance with any <code>Accept-Encoding</code> header in the request.</p>
|
||||||
|
<p>On failure, a client should treat a 404 NOT FOUND as indicating that it is up-to-date.
|
||||||
|
Clients should treat a 410 GONE as a synchronization error.
|
||||||
|
If the client has pending changes to send to the server, based on a now-removed version, then those changes cannot be reconciled and will be lost.
|
||||||
|
The client should, optionally after consulting the user, download and apply the latest snapshot.</p>
|
||||||
|
<h3 id="addsnapshot-1"><a class="header" href="#addsnapshot-1">AddSnapshot</a></h3>
|
||||||
|
<p>The request is a <code>POST</code> to <code><origin>/v1/client/add-snapshot/<versionId></code>.
|
||||||
|
The request body contains the snapshot data, optionally encoded using any encoding supported by actix-web.
|
||||||
|
The content-type must be <code>application/vnd.taskchampion.snapshot</code>.</p>
|
||||||
|
<p>If the version is invalid, as described above, the response should be 400 BAD REQUEST.
|
||||||
|
The server response should be 200 OK on success.</p>
|
||||||
|
<h3 id="getsnapshot-1"><a class="header" href="#getsnapshot-1">GetSnapshot</a></h3>
|
||||||
|
<p>The request is a <code>GET</code> to <code><origin>/v1/client/snapshot</code>.</p>
|
||||||
|
<p>The response is a 200 OK.
|
||||||
|
The snapshot is returned in the response body, with content-type <code>application/vnd.taskchampion.snapshot</code>.
|
||||||
|
The version ID appears in the <code>X-Version-Id</code> header.
|
||||||
|
The response body may be encoded, in accordance with any <code>Accept-Encoding</code> header in the request.</p>
|
||||||
|
<p>After downloading and decrypting a snapshot, a client must replace its entire local task database with the content of the snapshot.
|
||||||
|
Any local operations that had not yet been synchronized must be discarded.
|
||||||
|
After the snapshot is applied, the client should begin the synchronization process again, starting from the snapshot version.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="snapshots.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="plans.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="snapshots.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="plans.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
201
taskchampion/sync.html
Normal file
201
taskchampion/sync.html
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Synchronization and the Sync Server - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html" class="active"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="synchronization-and-the-sync-server"><a class="header" href="#synchronization-and-the-sync-server">Synchronization and the Sync Server</a></h1>
|
||||||
|
<p>This section covers <em>synchronization</em> of <em>replicas</em> containing the same set of tasks.
|
||||||
|
A replica is can perform all operations locally without connecting to a sync server, then share those operations with other replicas when it connects.
|
||||||
|
Sync is a critical feature of TaskChampion, allowing users to consult and update the same task list on multiple devices, without requiring constant connection.</p>
|
||||||
|
<p>This is a complex topic, and the section is broken into several chapters, beginning at the lower levels of the implementation and working up.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="tasks.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync-model.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="tasks.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync-model.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
217
taskchampion/taskdb.html
Normal file
217
taskchampion/taskdb.html
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Task Database - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html" class="active"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="task-database"><a class="header" href="#task-database">Task Database</a></h1>
|
||||||
|
<p>The task database is a layer of abstraction above the replica storage layer, responsible for maintaining some important invariants.
|
||||||
|
While the storage is pluggable, there is only one implementation of the task database.</p>
|
||||||
|
<h2 id="reading-data"><a class="header" href="#reading-data">Reading Data</a></h2>
|
||||||
|
<p>The task database provides read access to the data in the replica's storage through a variety of methods on the struct.
|
||||||
|
Each read operation is executed in a transaction, so data may not be consistent between read operations.
|
||||||
|
In practice, this is not an issue for TaskChampion's purposes.</p>
|
||||||
|
<h2 id="working-set"><a class="header" href="#working-set">Working Set</a></h2>
|
||||||
|
<p>The task database maintains the working set.
|
||||||
|
The working set maps small integers to current tasks, for easy reference by command-line users.
|
||||||
|
This is done in such a way that the task numbers remain stable until the working set is rebuilt, at which point gaps in the numbering, such as for completed tasks, are removed by shifting all higher-numbered tasks downward.</p>
|
||||||
|
<p>The working set is not replicated, and is not considered a part of any consistency guarantees in the task database.</p>
|
||||||
|
<h2 id="modifying-data"><a class="header" href="#modifying-data">Modifying Data</a></h2>
|
||||||
|
<p>Modifications to the data set are made by applying operations.
|
||||||
|
Operations are described in <a href="./storage.html">Replica Storage</a>.</p>
|
||||||
|
<p>Each operation is added to the list of operations in the storage, and simultaneously applied to the tasks in that storage.
|
||||||
|
Operations are checked for validity as they are applied.</p>
|
||||||
|
<h2 id="deletion-and-expiration"><a class="header" href="#deletion-and-expiration">Deletion and Expiration</a></h2>
|
||||||
|
<p>Deletion of a task merely changes the task's status to "deleted", leaving it in the Task database.
|
||||||
|
Actual removal of tasks from the task database takes place as part of <em>expiration</em>, triggered by the user as part of a garbage-collection process.
|
||||||
|
Expiration removes tasks with a <code>modified</code> property more than 180 days in the past, by creating a <code>Delete(uuid)</code> operation.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="storage.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="tasks.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="storage.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="tasks.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
240
taskchampion/tasks.html
Normal file
240
taskchampion/tasks.html
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<!DOCTYPE HTML>
|
||||||
|
<html lang="en" class="sidebar-visible no-js ayu">
|
||||||
|
<head>
|
||||||
|
<!-- Book generated using mdBook -->
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Tasks - TaskChampion</title>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Custom HTML head -->
|
||||||
|
|
||||||
|
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta name="description" content="">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
|
||||||
|
<link rel="icon" href="favicon.svg">
|
||||||
|
<link rel="shortcut icon" href="favicon.png">
|
||||||
|
<link rel="stylesheet" href="css/variables.css">
|
||||||
|
<link rel="stylesheet" href="css/general.css">
|
||||||
|
<link rel="stylesheet" href="css/chrome.css">
|
||||||
|
<link rel="stylesheet" href="css/print.css" media="print">
|
||||||
|
|
||||||
|
<!-- Fonts -->
|
||||||
|
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
|
||||||
|
<link rel="stylesheet" href="fonts/fonts.css">
|
||||||
|
|
||||||
|
<!-- Highlight.js Stylesheets -->
|
||||||
|
<link rel="stylesheet" href="highlight.css">
|
||||||
|
<link rel="stylesheet" href="tomorrow-night.css">
|
||||||
|
<link rel="stylesheet" href="ayu-highlight.css">
|
||||||
|
|
||||||
|
<!-- Custom theme stylesheets -->
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!-- Provide site root to javascript -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var path_to_root = "";
|
||||||
|
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "ayu";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Work around some values being stored in localStorage wrapped in quotes -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
try {
|
||||||
|
var theme = localStorage.getItem('mdbook-theme');
|
||||||
|
var sidebar = localStorage.getItem('mdbook-sidebar');
|
||||||
|
|
||||||
|
if (theme.startsWith('"') && theme.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
|
||||||
|
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
|
||||||
|
}
|
||||||
|
} catch (e) { }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Set the theme before any content is loaded, prevents flash -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var theme;
|
||||||
|
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
|
||||||
|
if (theme === null || theme === undefined) { theme = default_theme; }
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
html.classList.remove('no-js')
|
||||||
|
html.classList.remove('ayu')
|
||||||
|
html.classList.add(theme);
|
||||||
|
html.classList.add('js');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Hide / unhide sidebar before it is displayed -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
var html = document.querySelector('html');
|
||||||
|
var sidebar = 'hidden';
|
||||||
|
if (document.body.clientWidth >= 1080) {
|
||||||
|
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
|
||||||
|
sidebar = sidebar || 'visible';
|
||||||
|
}
|
||||||
|
html.classList.remove('sidebar-visible');
|
||||||
|
html.classList.add("sidebar-" + sidebar);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
|
||||||
|
<div class="sidebar-scrollbox">
|
||||||
|
<ol class="chapter"><li class="chapter-item expanded "><a href="installation.html"><strong aria-hidden="true">1.</strong> Installation</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="running-sync-server.html"><strong aria-hidden="true">1.1.</strong> Running the Sync Server</a></li></ol></li><li class="chapter-item expanded "><a href="internals.html"><strong aria-hidden="true">2.</strong> Internal Details</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="data-model.html"><strong aria-hidden="true">2.1.</strong> Data Model</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="storage.html"><strong aria-hidden="true">2.1.1.</strong> Replica Storage</a></li><li class="chapter-item expanded "><a href="taskdb.html"><strong aria-hidden="true">2.1.2.</strong> Task Database</a></li><li class="chapter-item expanded "><a href="tasks.html" class="active"><strong aria-hidden="true">2.1.3.</strong> Tasks</a></li></ol></li><li class="chapter-item expanded "><a href="sync.html"><strong aria-hidden="true">2.2.</strong> Synchronization and the Sync Server</a></li><li><ol class="section"><li class="chapter-item expanded "><a href="sync-model.html"><strong aria-hidden="true">2.2.1.</strong> Synchronization Model</a></li><li class="chapter-item expanded "><a href="snapshots.html"><strong aria-hidden="true">2.2.2.</strong> Snapshots</a></li><li class="chapter-item expanded "><a href="sync-protocol.html"><strong aria-hidden="true">2.2.3.</strong> Server-Replica Protocol</a></li><li class="chapter-item expanded "><a href="plans.html"><strong aria-hidden="true">2.2.4.</strong> Planned Functionality</a></li></ol></li></ol></li></ol> </div>
|
||||||
|
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="page-wrapper" class="page-wrapper">
|
||||||
|
|
||||||
|
<div class="page">
|
||||||
|
|
||||||
|
<div id="menu-bar-hover-placeholder"></div>
|
||||||
|
<div id="menu-bar" class="menu-bar sticky bordered">
|
||||||
|
<div class="left-buttons">
|
||||||
|
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
|
||||||
|
<i class="fa fa-bars"></i>
|
||||||
|
</button>
|
||||||
|
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
|
||||||
|
<i class="fa fa-paint-brush"></i>
|
||||||
|
</button>
|
||||||
|
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
|
||||||
|
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu (default)</button></li>
|
||||||
|
</ul>
|
||||||
|
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="menu-title">TaskChampion</h1>
|
||||||
|
|
||||||
|
<div class="right-buttons">
|
||||||
|
<a href="print.html" title="Print this book" aria-label="Print this book">
|
||||||
|
<i id="print-button" class="fa fa-print"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="search-wrapper" class="hidden">
|
||||||
|
<form id="searchbar-outer" class="searchbar-outer">
|
||||||
|
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
|
||||||
|
</form>
|
||||||
|
<div id="searchresults-outer" class="searchresults-outer hidden">
|
||||||
|
<div id="searchresults-header" class="searchresults-header"></div>
|
||||||
|
<ul id="searchresults">
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
|
||||||
|
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
|
||||||
|
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
|
||||||
|
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="content" class="content">
|
||||||
|
<main>
|
||||||
|
<h1 id="tasks"><a class="header" href="#tasks">Tasks</a></h1>
|
||||||
|
<p>Tasks are stored internally as a key/value map with string keys and values.
|
||||||
|
All fields are optional: the <code>Create</code> operation creates an empty task.
|
||||||
|
Display layers should apply appropriate defaults where necessary.</p>
|
||||||
|
<h2 id="atomicity"><a class="header" href="#atomicity">Atomicity</a></h2>
|
||||||
|
<p>The synchronization process does not support read-modify-write operations.
|
||||||
|
For example, suppose tags are updated by reading a list of tags, adding a tag, and writing the result back.
|
||||||
|
This would be captured as an <code>Update</code> operation containing the amended list of tags.
|
||||||
|
Suppose two such <code>Update</code> operations are made in different replicas and must be reconciled:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag1", "2020-11-23T14:21:22Z")</code></li>
|
||||||
|
<li><code>Update("d394be59-60e6-499e-b7e7-ca0142648409", "tags", "oldtag,newtag2", "2020-11-23T15:08:57Z")</code></li>
|
||||||
|
</ul>
|
||||||
|
<p>The result of this reconciliation will be <code>oldtag,newtag2</code>, while the user almost certainly intended <code>oldtag,newtag1,newtag2</code>.</p>
|
||||||
|
<p>The key names given below avoid this issue, allowing user updates such as adding a tag or deleting a dependency to be represented in a single <code>Update</code> operation.</p>
|
||||||
|
<h2 id="validity"><a class="header" href="#validity">Validity</a></h2>
|
||||||
|
<p><em>Any</em> key/value map is a valid task.
|
||||||
|
Consumers of task data must make a best effort to interpret any map, even if it contains apparently contradictory information.
|
||||||
|
For example, a task with status "completed" but no "end" key present should be interpreted as completed at an unknown time.</p>
|
||||||
|
<h2 id="representations"><a class="header" href="#representations">Representations</a></h2>
|
||||||
|
<p>Integers are stored in decimal notation.</p>
|
||||||
|
<p>Timestamps are stored as UNIX epoch timestamps, in the form of an integer.</p>
|
||||||
|
<h2 id="keys"><a class="header" href="#keys">Keys</a></h2>
|
||||||
|
<p>The following keys, and key formats, are defined:</p>
|
||||||
|
<ul>
|
||||||
|
<li><code>status</code> - one of <code>P</code> for a pending task (the default), <code>C</code> for completed, <code>D</code> for deleted, or <code>R</code> for recurring</li>
|
||||||
|
<li><code>description</code> - the one-line summary of the task</li>
|
||||||
|
<li><code>modified</code> - the time of the last modification of this task</li>
|
||||||
|
<li><code>start</code> - the most recent time at which this task was started (a task with no <code>start</code> key is not active)</li>
|
||||||
|
<li><code>end</code> - if present, the time at which this task was completed or deleted (note that this key may not agree with <code>status</code>: it may be present for a pending task, or absent for a deleted or completed task)</li>
|
||||||
|
<li><code>tag_<tag></code> - indicates this task has tag <code><tag></code> (value is an empty string)</li>
|
||||||
|
<li><code>wait</code> - indicates the time before which this task should be hidden, as it is not actionable</li>
|
||||||
|
<li><code>entry</code> - the time at which the task was created</li>
|
||||||
|
<li><code>annotation_<timestamp></code> - value is an annotation created at the given time</li>
|
||||||
|
</ul>
|
||||||
|
<p>Note that while TaskChampion recognizes "recurring" as a status, it does not implement recurrence directly.</p>
|
||||||
|
<h3 id="udas"><a class="header" href="#udas">UDAs</a></h3>
|
||||||
|
<p>Any unrecognized keys are treated as "user-defined attributes" (UDAs).
|
||||||
|
These attributes can be used to store additional data associated with a task.
|
||||||
|
For example, applications that synchronize tasks with other systems such as calendars or team planning services might store unique identifiers for those systems as UDAs.
|
||||||
|
The application defining a UDA defines the format of the value.</p>
|
||||||
|
<p>UDAs <em>should</em> have a namespaced structure of the form <code><namespace>.<key></code>, where <code><namespace></code> identifies the application defining the UDA.
|
||||||
|
For example, a service named "DevSync" synchronizing tasks from GitHub might use UDAs like <code>devsync.github.issue-id</code>.
|
||||||
|
Note that many existing UDAs for Taskwarrior integrations do not follow this pattern; these are referred to as legacy UDAs.</p>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<nav class="nav-wrapper" aria-label="Page navigation">
|
||||||
|
<!-- Mobile navigation buttons -->
|
||||||
|
<a rel="prev" href="taskdb.html" class="mobile-nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync.html" class="mobile-nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div style="clear: both"></div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-wide-wrapper" aria-label="Page navigation">
|
||||||
|
<a rel="prev" href="taskdb.html" class="nav-chapters previous" title="Previous chapter" aria-label="Previous chapter" aria-keyshortcuts="Left">
|
||||||
|
<i class="fa fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a rel="next" href="sync.html" class="nav-chapters next" title="Next chapter" aria-label="Next chapter" aria-keyshortcuts="Right">
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.playground_copyable = true;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<script src="elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="mark.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="searcher.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<script src="clipboard.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="highlight.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script src="book.js" type="text/javascript" charset="utf-8"></script>
|
||||||
|
|
||||||
|
<!-- Custom JS scripts -->
|
||||||
|
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
104
taskchampion/tomorrow-night.css
Normal file
104
taskchampion/tomorrow-night.css
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/* Tomorrow Night Theme */
|
||||||
|
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||||
|
/* Original theme - https://github.com/chriskempson/tomorrow-theme */
|
||||||
|
/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */
|
||||||
|
|
||||||
|
/* Tomorrow Comment */
|
||||||
|
.hljs-comment {
|
||||||
|
color: #969896;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Red */
|
||||||
|
.hljs-variable,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-tag,
|
||||||
|
.hljs-regexp,
|
||||||
|
.ruby .hljs-constant,
|
||||||
|
.xml .hljs-tag .hljs-title,
|
||||||
|
.xml .hljs-pi,
|
||||||
|
.xml .hljs-doctype,
|
||||||
|
.html .hljs-doctype,
|
||||||
|
.css .hljs-id,
|
||||||
|
.css .hljs-class,
|
||||||
|
.css .hljs-pseudo {
|
||||||
|
color: #cc6666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Orange */
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-preprocessor,
|
||||||
|
.hljs-pragma,
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-literal,
|
||||||
|
.hljs-params,
|
||||||
|
.hljs-constant {
|
||||||
|
color: #de935f;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Yellow */
|
||||||
|
.ruby .hljs-class .hljs-title,
|
||||||
|
.css .hljs-rule .hljs-attribute {
|
||||||
|
color: #f0c674;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Green */
|
||||||
|
.hljs-string,
|
||||||
|
.hljs-value,
|
||||||
|
.hljs-inheritance,
|
||||||
|
.hljs-header,
|
||||||
|
.hljs-name,
|
||||||
|
.ruby .hljs-symbol,
|
||||||
|
.xml .hljs-cdata {
|
||||||
|
color: #b5bd68;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Aqua */
|
||||||
|
.hljs-title,
|
||||||
|
.css .hljs-hexcolor {
|
||||||
|
color: #8abeb7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Blue */
|
||||||
|
.hljs-function,
|
||||||
|
.python .hljs-decorator,
|
||||||
|
.python .hljs-title,
|
||||||
|
.ruby .hljs-function .hljs-title,
|
||||||
|
.ruby .hljs-title .hljs-keyword,
|
||||||
|
.perl .hljs-sub,
|
||||||
|
.javascript .hljs-title,
|
||||||
|
.coffeescript .hljs-title {
|
||||||
|
color: #81a2be;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tomorrow Purple */
|
||||||
|
.hljs-keyword,
|
||||||
|
.javascript .hljs-function {
|
||||||
|
color: #b294bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
background: #1d1f21;
|
||||||
|
color: #c5c8c6;
|
||||||
|
padding: 0.5em;
|
||||||
|
-webkit-text-size-adjust: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coffeescript .javascript,
|
||||||
|
.javascript .xml,
|
||||||
|
.tex .hljs-formula,
|
||||||
|
.xml .javascript,
|
||||||
|
.xml .vbscript,
|
||||||
|
.xml .css,
|
||||||
|
.xml .hljs-cdata {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition {
|
||||||
|
color: #718c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion {
|
||||||
|
color: #c82829;
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue