tp-note 1.17.0

Minimalistic note taking: save and edit your clipboard content as a note file
//! Set configuration defaults, reads and writes _Tp-Note_'s configuration file
//! and exposes the configuration as `static` variable.

use crate::error::FileError;
use crate::filename;
use crate::settings::ARGS;
use crate::VERSION;
use directories::ProjectDirs;
use lazy_static::lazy_static;
use log::LevelFilter;
#[cfg(not(test))]
use sanitize_filename_reader_friendly::TRIM_LINE_CHARS;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
#[cfg(not(test))]
use std::fs::File;
#[cfg(not(test))]
use std::io::Write;
use std::path::Path;
use std::path::PathBuf;
use std::sync::RwLock;

/// Name of this executable (without the Windows ".exe" extension).
const CURRENT_EXE: &str = "tpnote";

/// Tp-Note's configuration file filename.
const CONFIG_FILENAME: &str = "tpnote.toml";

/// Default value for the command line option `--debug`.  Determines the maximum
/// debug level events must have, to be logged.  If the command line option
/// `--debug` is present, its value will be used instead.
const ARG_DEFAULT_DEBUG: LevelFilter = LevelFilter::Error;

/// Default value for the command line flag `--edit` to disable file watcher,
/// (Markdown)-renderer, html server and a web browser launcher set to `true`.
const ARG_DEFAULT_EDITOR: bool = false;

/// Default value for the command line flag `--no-filename-sync` to disable
/// the title to filename synchronisation mechanism permanently.
/// If set to `true`, the corresponding command line flag is ignored.
const ARG_DEFAULT_NO_FILENAME_SYNC: bool = false;

/// Default value for the command line flag `--popup`. If the command line flag
/// `--popup` or `POPUP` is `true`, all log events will also trigger the
/// appearance of a popup alert window.  Note, that error level debug events
/// will always pop up, regardless of `--popup` and `POPUP` (unless
/// `--debug=off`).
const ARG_DEFAULT_POPUP: bool = true;

/// Default value for the command line flag `--tty`. _Tp-Note_ tries different
/// heuristics to detect weather a graphic environment is available or not. For
/// example, under Linux, the '`DISPLAY`' environment variable is evaluated. The
/// '`--tty`' flag disables the automatic detection and sets _Tp-Note_ in
/// "console" mode, where only the non GUI editor (see configuration variable:
/// '`[app_args] editor_console`') and no viewer is launched. If this is set
/// to `true` _Tp-Note_ starts in console mode permanently.
const ARG_DEFAULT_TTY: bool = false;

/// Default value for the command line flag `--add-header`. If unset,
/// _Tp-Note_ exits of when it tries to open a text file without a YAML
/// header. When this flag is set, the missing header is constructed by
/// means of the text file's filename and creation date.
const ARG_DEFAULT_ADD_HEADER: bool = true;

/// Maximum length of a note's filename in bytes. If a filename template produces
/// a longer string, it will be truncated.
#[cfg(not(test))]
pub const FILENAME_LEN_MAX: usize =
    // Most file system's limit.
    255
    // Additional separator.
    - 1
    // Additional copy counter.
    - FILENAME_COPY_COUNTER_OPENING_BRACKETS.len() - 2 - FILENAME_COPY_COUNTER_CLOSING_BRACKETS.len()
    // Extra spare bytes, in case the user's copy counter is longer.
    - 6;
#[cfg(test)]
pub const FILENAME_LEN_MAX: usize = 10;

/// List of characters that can be part of a _sort tag_.
/// This list must not include `SORT_TAG_EXTRA_SEPARATOR`.
/// The first character in the filename which is not
/// in this list, marks the end of the sort tag.
const FILENAME_SORT_TAG_CHARS: &str = "0123456789.-_ \t";

/// In case the file stem starts with a character in
/// `SORT_TAG_CHARS` the `SORT_TAG_EXTRA_SEPARATOR`
/// character is inserted in order to separate both parts
/// when the filename is read next time.
const FILENAME_SORT_TAG_EXTRA_SEPARATOR: char = '\'';

/// If the stem of a filename ends with a pattern, that is
/// similar to a copy counter, add this extra separator. It
/// must be one of `TRIM_LINE_CHARS` (see definition in
/// crate: `sanitize_filename_reader_friendly`) because they
/// are known not to appear at the end of `sanitze()`'d
/// strings. This is why they are suitable here.
const FILENAME_COPY_COUNTER_EXTRA_SEPARATOR: char = '-';

/// Tp-Note may add a counter at the end of the filename when
/// it can not save a file because the name is taken already.
/// This is the opening bracket search pattern. Some examples:
/// `"-"`, "'_'"", `"_-"`,`"-_"`, `"("`
/// Can be empty.
const FILENAME_COPY_COUNTER_OPENING_BRACKETS: &str = "(";

/// Tp-Note may add a counter at the end of the filename when
/// it can not save a file because the name is taken already.
/// This is the closing bracket search pattern. Some examples:
/// `"-"`, "'_'"", `"_-"`,`"-_"`, `"("`
/// Can be empty.
const FILENAME_COPY_COUNTER_CLOSING_BRACKETS: &str = ")";

/// When a filename is taken already, Tp-Note adds a copy
/// counter number in the range of `0..COPY_COUNTER_MAX`
/// at the end.
pub const FILENAME_COPY_COUNTER_MAX: usize = 400;

/// File extension of new _Tp-Note_ files.
///
/// For Unix-like systems this defaults to `.md` because all the
/// listed file editors (see `APP_ARGS_EDITOR`) support it. The
/// Windows default is `.txt` to ensure that the _Notepad_ editor can
/// handle these files properly.
///
/// As longs as all extensions are part of the same group, here
/// `FILENAME_EXTENSIONS_MD`, all note files are interpreted as
/// _Markdown_ on all systems.
///
/// NB: Do not forget to adapt the templates `TMPL_*` in case you set
/// this to another markup language.
#[cfg(all(target_family = "unix", not(target_vendor = "apple")))]
pub const FILENAME_EXTENSION_DEFAULT: &str = "md";
#[cfg(target_family = "windows")]
pub const FILENAME_EXTENSION_DEFAULT: &str = "txt";
#[cfg(all(target_family = "unix", target_vendor = "apple"))]
pub const FILENAME_EXTENSION_DEFAULT: &str = "md";

/// The variables `FILENAME_EXTENSIONSS_*` list file extensions that Tp-Note
/// considers as its own note files.
/// Tp-Note opens these files, reads their their YAML header and
/// launches an external file editor and an file viewer
/// (web browser).
/// According to the markup language used, the appropriate
/// renderer is called to convert the note's content into HTML.
/// The rendered HTML is then shown to the user with his
/// web browser.
///
/// The present list contains file extensions of
/// Markdown encoded Tp-Note files.
pub const FILENAME_EXTENSIONS_MD: &[&str] = &["txt", "md", "markdown", "markdn", "mdown", "mdtxt"];

/// The present list contains file extensions of
/// RestructuredText encoded Tp-Note files.
///
/// See also `FILENAME_EXTENSIONS_MD`.
pub const FILENAME_EXTENSIONS_RST: &[&str] = &["rst", "rest"];

/// The present list contains file extensions of
/// HTML encoded Tp-Note files. For these
/// file types their content is forwarded to the web browser
/// without modification.
///
/// See also `FILENAME_EXTENSIONS_MD`.
pub const FILENAME_EXTENSIONS_HTML: &[&str] = &["htmlnote"];

/// The present list contains file extensions of
/// Text encoded Tp-Note files that the viewer shows
/// literally without (almost) any additional rendering.
/// Only hyperlinks in _Markdown_, _reStructuredText_, _Asciidoc_ and _HTML_ are
/// rendered, thus clickable.
///
/// See also `FILENAME_EXTENSIONS_MD`.
pub const FILENAME_EXTENSIONS_TXT: &[&str] = &["txtnote", "adoc", "asciidoc", "mediawiki", "mw"];

/// The present list contains file extensions of
/// Tp-Note files for which no viewer is opened
/// (unless Tp-Note is invoked with `--view`).
///
/// See also `FILENAME_EXTENSIONS_MD`.
pub const FILENAME_EXTENSIONS_NO_VIEWER: &[&str] = &["t2t"];

/// This a dot by definition.
pub(crate) const FILENAME_DOTFILE_MARKER: char = '.';

/// By default clipboard support is enabled, can be disabled
/// in config file. A false value here will set ENABLE_EMPTY_CLIPBOARD to
/// false.
const CLIPBOARD_READ_ENABLED: bool = true;

/// Should the clipboard be emptied when tp-note closes?
/// Default value.
const CLIPBOARD_EMPTY_ENABLED: bool = true;

/// As all application logic is encoded in Tp-Note's templates, it does not know about field names.
/// Nevertheless it is useful to identify at least one field as _the_ field that identifies a note
/// the most.  When `TMPL_COMPULSORY_HEADER_FIELD` is not empty, Tp-Note will not synchronize the
/// note's filename and will pop up an error message, unless it finds the field in the note's
/// header.  When `TMPL_COMPULSORY_HEADER_FIELD` is empty, all files are synchronized without any
/// further field check. Make sure to define a default value with `fm_* | default(value=*)`
/// in case the variable `fm_*` does not exist in the note's front matter.
const TMPL_COMPULSORY_HEADER_FIELD: &str = "title";

/// Default content template used when the command line argument <sanit> is a directory. Can be
/// changed through editing the configuration file.
/// The following variables are  defined:
/// `{{ sanit | stem }}`, `{{ path | stem }}`, `{{ path | ext }}`, `{{ extension_default }}` `{{
/// file | tag }}`, `{{ username }}`, `{{ date }}`, `{{ lang }}`, `{{ dir_path }}`.
/// In addition all environment variables can be used, e.g.  `{{ get_env(name=\"LOGNAME\") }}`
/// When placed in YAML front matter, the filter `| json_encode` must be appended to each variable.
const TMPL_NEW_CONTENT: &str = "\
---
title:      {{ dir_path | trim_tag | cut | json_encode }}
subtitle:   {{ 'Note' | json_encode }}
author:     {{ username | json_encode }}
date:       {{ now() | date(format='%Y-%m-%d') | json_encode }}
lang:       {{ lang | json_encode }}
---


";

/// Default filename template for a new note file on disk. It implements the sync criteria for
/// note metadata in front matter and filename.
/// Useful variables in this context are:
/// `{{ title| sanit }}`, `{{ subtitle| sanit }}`, `{{ extension_default }}`,
/// All variables also exist in a `{{ <var>| sanit(alpha) }}` variant: in case its value starts
/// with a number, the string is prepended with `'`.  The first non-numerical variable must be some
/// `{{ <var>| sanit(alpha) }}` variant.
/// Note, as this is filename template, all variables (except `now` and `extension_default` must be
/// filtered by a `sanit` or `sanit(alpha=true)` filter.
const TMPL_NEW_FILENAME: &str = "\
{{ now() | date(format='%Y%m%d-') }}\
{{ fm_title | sanit(alpha=true) }}{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit  }}{{ extension_default | prepend_dot }}\
";

/// Default template used, when the clipboard or the input stream `stdin` contains a string and one
/// the of these strings contains a valid YAML front matter section.
/// The clipboards body is in `{{ clipboard }}`, the header is in `{{ clipboard_header }}`.  The
/// stdin's body is in `{{ stdin }}`, the header is in `{{ stdin_header }}`.
/// First all variables defined in the clipboard's front matter are registered, the ones
/// defined in the input stream `stdin`. The latter can overwrite the former.  One of the front
/// matters must define the `title` variable, which is then available in this template as `{{
/// fm_title }}`.
/// When placed in YAML front matter, the filter `| json_encode` must be
/// appended to each variable.
const TMPL_FROM_CLIPBOARD_YAML_CONTENT: &str = "\
---
title:      {{ fm_title | default(value = path|trim_tag) | cut | json_encode }}
subtitle:   {{ fm_subtitle | default(value = 'Note') | cut | json_encode }}
author:     {{ fm_author | default(value=username) | json_encode }}
date:       {{ fm_date | default(value = now()|date(format='%Y-%m-%d')) | json_encode }}
lang:       {{ fm_lang | default(value = lang) | json_encode }}
{% for k, v in fm_all\
 | remove(var='fm_title')\
 | remove(var='fm_subtitle')\
 | remove(var='fm_author')\
 | remove(var='fm_date')\
 | remove(var='fm_lang') %}\
{{ k }}:\t\t{{ v | json_encode }}
{% endfor %}\
---

{{ stdin ~ clipboard }}

";

/// Default filename template used when the stdin or the clipboard contains a string and one of
/// them has a valid YAML header.
const TMPL_FROM_CLIPBOARD_YAML_FILENAME: &str = "\
{{ fm_sort_tag | default(value = now() | date(format='%Y%m%d-')) }}\
{{ fm_title | sanit(alpha=true) }}\
{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit  }}\
{{ fm_file_ext | default(value = extension_default ) | prepend_dot }}\
";

/// Default template used, when the clipboard or the input stream `stdin` contains a string and
/// this string has no valid YAML front matter section.  The clipboards content is in `{{ clipboard
/// }}`, its truncated version in `{{ clipboard | heading }}` When the clipboard contains a
/// hyperlink in Markdown or reStruncturedText format. See crate `parse-hyperlinks` for details.
/// For example: `[<link-name>](<link-url> "link-title")`, can be accessed with the variables:
/// `{{ clipboard | linkname }}`, `{{ clipboard | linktarget }}` and `{{ clipboard | linkttitle }}`.
const TMPL_FROM_CLIPBOARD_CONTENT: &str = "\
{%- set lname = stdin ~ clipboard | linkname -%}
{%- set ok_linkname = lname !=''\
    and not lname is starting_with(\"http\")\
    and not lname is starting_with(\"HTTP\") -%}
---
{% if ok_linkname %}\
title:      {{ stdin ~ clipboard | linkname | cut | json_encode }}
{% else %}\
title:      {{ stdin ~ clipboard | heading | cut | json_encode }}
{% endif %}\
{% if stdin ~ clipboard | linkname !='' and stdin ~ clipboard | cut | linebreaksbr == stdin ~ clipboard | cut %}\
subtitle:   {{ 'URL' | json_encode }}
{% else %}\
subtitle:   {{ 'Note' | json_encode }}
{% endif %}\
author:     {{ username | json_encode }}
date:       {{ now() | date(format='%Y-%m-%d') | json_encode }}
lang:       {{ lang | json_encode }}
---

{{ stdin ~ clipboard }}

";

/// Default filename template used when the stdin ~ clipboard contains a string.
const TMPL_FROM_CLIPBOARD_FILENAME: &str = "\
{{ now() | date(format='%Y%m%d-') }}\
{{ fm_title | sanit(alpha=true) }}\
{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit  }}{{ extension_default | prepend_dot }}\
";

/// Default template used, when the opened text file (with a known file
/// extension) is missing a YAML front matter section. This template prepends
/// such a section. The template inserts information extracted from the input
/// filename and its creation date.
const TMPL_FROM_TEXT_FILE_CONTENT: &str = "\
---
title:      {{ path | stem | split(pat='--') | first | cut | json_encode }}
subtitle:   {{ path | stem | split(pat='--') | nth(n=1) | cut | json_encode }}
author:     {{ username | json_encode }}
date:       {{ path_file_date | date(format='%Y-%m-%d') | json_encode }}
lang:       {{ lang | json_encode }}
orig_name:  {{ path | filename | json_encode }}
---

{{ path_file_text }}
";

/// Default filename template used when the input file (with a known
/// file extension) is missing a YAML front matter section.
/// The text file's sort-tag and file extension are preserved.
const TMPL_FROM_TEXT_FILE_FILENAME: &str = "\
{% if path | tag == '' %}{{ path_file_date | date(format='%Y%m%d-') }}\
{% else %}{{ path | tag }}{% endif %}\
{{ fm_title | sanit(alpha=true) }}\
{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit  }}\
{{ path | ext | prepend_dot }}\
";

/// Default template used when the command line <path> parameter points to an existing
/// non-`.md`-file. Can be modified through editing the configuration file.
const TMPL_ANNOTATE_FILE_CONTENT: &str = "\
---
title:      {{ path | trim_tag | json_encode }}
{% if stdin ~ clipboard | linkname !='' and stdin ~ clipboard | heading == stdin ~ clipboard %}\
subtitle:   {{ 'URL' | json_encode }}
{% else %}\
subtitle:   {{ 'Note' | json_encode }}
{% endif %}\
author:     {{ username | json_encode }}
date:       {{ now() | date(format='%Y-%m-%d') | json_encode }}
lang:       {{ lang | json_encode }}
---

[{{ path | filename }}](<{{ path | filename }}>)
{% if stdin ~ clipboard != '' %}{% if stdin ~ clipboard != stdin ~ clipboard | heading %}
---
{% endif %}
{{ stdin ~ clipboard }}
{% endif %}
";

/// Filename of a new note, that annotates an existing file on disk given in
/// <path>.
const TMPL_ANNOTATE_FILE_FILENAME: &str = "\
{{ path | tag }}{{ fm_title | sanit(alpha=true) }}\
{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit }}{{ extension_default | prepend_dot }}\
";

/// Default filename template to test, if the filename of an existing note file on disk,
/// corresponds to the note's meta data stored in its front matter. If it is not the case, the
/// note's filename will be renamed.  Can be modified through editing the configuration file.
const TMPL_SYNC_FILENAME: &str = "\
{{ fm_sort_tag | default(value = path | tag) }}\
{{ fm_title | default(value='No title') | sanit(alpha=true) }}\
{% if fm_subtitle | default(value='') | sanit != '' %}--{% endif %}\
{{ fm_subtitle | default(value='') | sanit  }}\
{{ fm_file_ext | default(value = path | ext) | prepend_dot }}\
";

/// Default command line argument list when launching the web browser.
/// The list is executed item by item until an installed web browser is found.
/// Can be changed in config file.
#[cfg(all(target_family = "unix", not(target_vendor = "apple")))]
const APP_ARGS_BROWSER: &[&[&str]] = &[
    &[
        "flatpak",
        "run",
        "org.mozilla.firefox",
        "--new-window",
        "--private-window",
    ],
    &["firefox", "--new-window", "--private-window"],
    &["firefox-esr", "--new-window", "--private-window"],
    &[
        "flatpak",
        "run",
        "com.github.Eloston.UngoogledChromium",
        "--new-window",
        "--incognito",
    ],
    &[
        "flatpak",
        "run",
        "org.chromium.Chromium",
        "--new-window",
        "--incognito",
    ],
    &["chromium-browser", "--new-window", "--incognito"],
    &["chrome", "--new-window", "--incognito"],
];
#[cfg(target_family = "windows")]
const APP_ARGS_BROWSER: &[&[&str]] = &[
    &[
        "C:\\Program Files\\Mozilla Firefox\\firefox.exe",
        "--new-window",
        "--private-window",
    ],
    &[
        "C:\\Program Files\\Google\\Chrome\\Application\\chrome",
        "--new-window",
        "--incognito",
    ],
    &[
        "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
        "--inprivate",
    ],
];
// Some info about launching programs on iOS:
//[dshell.pdf](https://www.stata.com/manuals13/dshell.pdf)
#[cfg(all(target_family = "unix", target_vendor = "apple"))]
const APP_ARGS_BROWSER: &[&[&str]] = &[];

/// Default command line argument list when launching external editor.
/// The editor list is executed item by item until an editor is found.
/// Can be changed in config file.
#[cfg(all(target_family = "unix", not(target_vendor = "apple")))]
const APP_ARGS_EDITOR: &[&[&str]] = &[
    &["code", "-w", "-n"],
    &["flatpak", "run", "com.visualstudio.code", "-w", "-n"],
    &["atom", "-w"],
    &["marktext", "--no-sandbox", "--new-window"],
    &[
        "flatpak",
        "run",
        "com.github.marktext.marktext",
        "--new-window",
    ],
    // Disable Typora until bug fix:
    // https://github.com/typora/typora-issues/issues/4633
    //    &["typora"],
    &["retext"],
    &["geany", "-s", "-i", "-m"],
    &["gedit", "-w"],
    &["mousepad", "--disable-server"],
    &["leafpad"],
    &["nvim-qt", "--nofork"],
    &["gvim", "--nofork"],
];
#[cfg(target_family = "windows")]
const APP_ARGS_EDITOR: &[&[&str]] = &[
    // Disable Typora until bug fix:
    // https://github.com/typora/typora-issues/issues/4633
    //    &["C:\\Program Files\\Typora\\Typora.exe"],
    &[
        "C:\\Program Files\\Mark Text\\Mark Text.exe",
        "--new-window",
    ],
    &[
        "C:\\Program Files\\Notepad++\\notepad++.exe",
        "-nosession",
        "-multiInst",
    ],
    &["C:\\Windows\\notepad.exe"],
];
// Some info about launching programs on iOS:
//[dshell.pdf](https://www.stata.com/manuals13/dshell.pdf)
#[cfg(all(target_family = "unix", target_vendor = "apple"))]
const APP_ARGS_EDITOR: &[&[&str]] = &[
    &["code", "-w", "-n"],
    &["atom", "-w"],
    &["marktext", "--no-sandbox"],
    &["typora"],
    &["gvim", "--nofork"],
    &["mate"],
    &["open", "-a", "TextEdit"],
    &["open", "-a", "TextMate"],
    &["open"],
];

/// Default command line argument list when launching an external editor
/// and no graphical environment is available (`DISPLAY=''`).
/// This lists console file editors only.
/// The editor list is executed item by item until an editor is found.
/// Can be changed in config file.
#[cfg(all(target_family = "unix", not(target_vendor = "apple")))]
const APP_ARGS_EDITOR_CONSOLE: &[&[&str]] = &[&["nvim"], &["nano"], &["vim"], &["emacs"], &["vi"]];
#[cfg(target_family = "windows")]
const APP_ARGS_EDITOR_CONSOLE: &[&[&str]] = &[&[]];
// Some info about launching programs on iOS:
// [dshell.pdf](https://www.stata.com/manuals13/dshell.pdf)
#[cfg(all(target_family = "unix", target_vendor = "apple"))]
const APP_ARGS_EDITOR_CONSOLE: &[&[&str]] = &[
    &["nvim"],
    &["nano"],
    &["pico"],
    &["vim"],
    &["emacs"],
    &["vi"],
];

/// When Tp-Note starts, it launches two external applications: some text editor
/// and the viewer (web browser). By default the two programs are launched at
/// the same time (`VIEWER_STARTUP_DELAY==0`). If `VIEWER_STARTUP_DELAY>0` the
/// viewer (web browser) will be launched `VIEWER_STARTUP_DELAY` milliseconds
/// after the text editor. If `VIEWER_STARTUP_DELAY<0` the viewer will be
/// started first. Common values are `-1000`, `0` and `1000`.
const VIEWER_STARTUP_DELAY: isize = 500;

/// When set to true, the viewer feature is automatically disabled when
/// _Tp-Note_ encounters an `.md` file without header.  Experienced users can
/// set this to `true`. This setting is ignored, meaning is considered `false`,
/// if `ARG_DEFAULT_ADD_HEADER=true` or `ARGS.add_header=true` or
/// `ARGS.viewer=true`.
const VIEWER_MISSING_HEADER_DISABLES: bool = false;

/// How often should the file watcher check for changes?
/// Delay in milliseconds.
const VIEWER_NOTIFY_PERIOD: u64 = 1000;

/// The maximum number of TCP connections the HTTP server can handle at the same
/// time. In general, the serving and live update of the HTML rendition of the
/// note file, requires normally 3 TCP connections: 1 old event channel (that is
/// still open from the previous update), 1 TCP connection to serve the HTML,
/// the local images (and referenced documents), and 1 new event channel.  In
/// practise, stale connection are not always closed immediately. Hence 4 open
/// connections are not uncommon.
const VIEWER_TCP_CONNECTIONS_MAX: usize = 16;

/// The first entry per line is the file extension in lowercase(!), the second the
/// corresponding mime type.  Embedded files with types other than those listed
/// here are silently ignored.  Note, that image files must be located in the
/// same or in the note's parent directory.
const VIEWER_SERVED_MIME_TYPES: &[&[&str]] = &[
    &["apng", "image/apng"],
    &["avif", "image/avif"],
    &["bmp", "image/bmp"],
    &["gif", "image/gif"],
    &["html", "text/html"],
    &["htm", "text/html"],
    &["ico", "image/vnd.microsoft.icon"],
    &["jpeg", "image/jpeg"],
    &["jpg", "image/jpeg"],
    &["pdf", "application/pdf"],
    &["png", "image/png"],
    &["svg", "image/svg+xml"],
    &["tiff", "image/tiff"],
    &["tif", "image/tiff"],
    &["webp", "image/webp"],
    &["mp3", "audio/mp3"],
    &["ogg", "audio/ogg"],
    &["oga", "audio/ogg"],
    &["weba", "audio/webm"],
    &["flac", "audio/flac"],
    &["wav", "audio/wav"],
    &["opus", "audio/opus"],
    &["mp4", "video/mp4"],
    &["ogv", "video/ogg"],
    &["webm", "video/webm"],
    &["ogx", "application/ogg"],
];

/// Served file types with corresponding mime types.  First entry per line is
pub const VIEWER_RENDITION_TMPL: &str = r#"<!DOCTYPE html>
<html lang="{{ fm_lang | default(value='en') }}">
<head>
<meta charset="UTF-8">
<title>{{ fm_title }}</title>
<style>
table, th, td { font-weight: normal; }
table.center {
  margin-left: auto;
  margin-right: auto;
  background-color: #f3f2e4;
  border:1px solid grey;
}
th, td {
  padding: 3px;
  padding-left:15px;
  padding-right:15px;
}
th.key{ color:#444444; text-align:right; }
th.val{
  color:#316128;
  text-align:left;
  font-family:sans-serif;
}
th.keygrey{ color:grey; text-align:right; }
th.valgrey{ color:grey; text-align:left; }
pre { white-space: pre-wrap; }
em { color: #523626; }
a { color: #316128; }
h1 { font-size: 150% }
h2 { font-size: 132% }
h3 { font-size: 115% }
h4, h5, h6 { font-size: 100% }
h1, h2, h3, h4, h5, h6 { color: #263292; font-family:sans-serif; }

</style>
  </head>
  <body>
  <table class="center">
    <tr>
    <th class="key">title:</th>
    <th class="val"><b>{{ fm_title }}</b></th>
  </tr>
    <tr>
    <th class="key">subtitle:</th>
    <th class="val">{{ fm_subtitle | default(value='') }}</th>
  </tr>
    <tr>
    <th class="keygrey">date:</th>
    <th class="valgrey">{{ fm_date | default(value='') }}</th>
  </tr>
  {% for k, v in fm_all| remove(var='fm_title')| remove(var='fm_subtitle')| remove(var='fm_date') %}
    <tr>
    <th class="keygrey">{{ k }}:</th>
    <th class="valgrey">{{ v }}</th>
  </tr>
  {% endfor %}
  </table>
  <div class="note-body">{{ note_body }}</div>
  <script>{{ note_js }}</script>
</body>
</html>
"#;

/// HTML template to render the viewer-error page.
pub const VIEWER_ERROR_TMPL: &str = r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Syntax error</title>
<style>
.note-error { color: #523626; }
pre { white-space: pre-wrap; }
a { color: #316128; }
h1, h2, h3, h4, h5, h6 { color: #d3af2c; font-family:sans-serif; }
</style>
</head>
<body>
<h3>Syntax error</h3>
<p> in note file: <pre>{{ path }}</pre><p>
<div class="note-error">
<hr>
<pre>{{ note_error }}</pre>
<hr>
</div>
{{ note_erroneous_content }}
<script>{{ note_js }}</script>
</body>
</html>
"#;

/// Template used to render a note into html when the
/// rendition is saved to disk
pub const EXPORTER_RENDITION_TMPL: &str = r#"<!DOCTYPE html>
<html lang="{{ fm_lang | default(value='en') }}">
<head>
<meta charset="utf-8">
<title>{{ fm_title }}</title>
<style>
table, th, td { font-weight: normal; }
table.center {
  margin-left: auto;
  margin-right: auto;
  background-color: #f3f2e4;
  border:1px solid grey;
}
th, td {
  padding: 3px;
  padding-left:15px;
  padding-right:15px;
}
th.key{ color:#444444; text-align:right; }
th.val{
  color:#316128;
  text-align:left;
  font-family:sans-serif;
}
th.keygrey{ color:grey; text-align:right; }
th.valgrey{ color:grey; text-align:left; }
pre { white-space: pre-wrap; }
em { color: #523626; }
a { color: #316128; }
h1 { font-size: 150% }
h2 { font-size: 132% }
h3 { font-size: 115% }
h4, h5, h6 { font-size: 100% }
h1, h2, h3, h4, h5, h6 { color: #263292; font-family:sans-serif; }

</style>
  </head>
  <body>
  <table class="center">
    <tr>
    <th class="key">title:</th>
    <th class="val"><b>{{ fm_title }}</b></th>
  </tr>
    <tr>
    <th class="key">subtitle:</th>
    <th class="val">{{ fm_subtitle | default(value='') }}</th>
  </tr>
    <tr>
    <th class="keygrey">date:</th>
    <th class="valgrey">{{ fm_date | default(value='') }}</th>
  </tr>
  {% for k, v in fm_all| remove(var='fm_title')| remove(var='fm_subtitle')| remove(var='fm_date') %}
    <tr>
    <th class="keygrey">{{ k }}:</th>
    <th class="valgrey">{{ v }}</th>
  </tr>
  {% endfor %}
  </table>
  <div class="note-body">{{ note_body }}</div>
</body>
</html>
"#;

/// Configuration data, deserialized from the configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Cfg {
    /// Version number of the config file as String -or-
    /// a text message explaining why we could not load the
    /// configuration file.
    pub version: String,
    pub arg_default: ArgDefault,
    pub filename: Filename,
    pub clipboard: Clipboard,
    pub tmpl: Tmpl,
    pub app_args: AppArgs,
    pub viewer: Viewer,
    pub exporter: Exporter,
}

/// Command line arguments, deserialized form configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct ArgDefault {
    pub debug: LevelFilter,
    pub edit: bool,
    pub no_filename_sync: bool,
    pub popup: bool,
    pub tty: bool,
    pub add_header: bool,
}

/// Configuration of filename parsing, deserialized from the
/// configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Filename {
    pub sort_tag_chars: String,
    pub sort_tag_extra_separator: char,
    pub copy_counter_extra_separator: String,
    pub copy_counter_opening_brackets: String,
    pub copy_counter_closing_brackets: String,
    pub extension_default: String,
    pub extensions_md: Vec<String>,
    pub extensions_rst: Vec<String>,
    pub extensions_html: Vec<String>,
    pub extensions_txt: Vec<String>,
    pub extensions_no_viewer: Vec<String>,
}

/// Configuration of clipboard behaviour, deserialized from the
/// configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Clipboard {
    pub read_enabled: bool,
    pub empty_enabled: bool,
}

/// Filename templates and content templates, deserialized from the configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Tmpl {
    pub compulsory_header_field: String,
    pub new_content: String,
    pub new_filename: String,
    pub from_clipboard_yaml_content: String,
    pub from_clipboard_yaml_filename: String,
    pub from_clipboard_content: String,
    pub from_clipboard_filename: String,
    pub from_text_file_content: String,
    pub from_text_file_filename: String,
    pub annotate_file_content: String,
    pub annotate_file_filename: String,
    pub sync_filename: String,
}

/// Arguments lists for invoking external applications, deserialized from the
/// configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct AppArgs {
    pub browser: Vec<Vec<String>>,
    pub editor: Vec<Vec<String>>,
    pub editor_console: Vec<Vec<String>>,
}

/// Configuration data for the viewer feature, deserialized from the
/// configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Viewer {
    pub startup_delay: isize,
    pub missing_header_disables: bool,
    pub notify_period: u64,
    pub tcp_connections_max: usize,
    pub served_mime_types: Vec<Vec<String>>,
    pub rendition_tmpl: String,
    pub error_tmpl: String,
}

/// Configuration for the HTML exporter feature, deserialized from the
/// configuration file.
#[derive(Debug, Serialize, Deserialize)]
pub struct Exporter {
    pub rendition_tmpl: String,
}

/// When no configuration file is found, defaults are set here from built-in
/// constants. These defaults are then serialized into a newly created
/// configuration file on disk.
impl ::std::default::Default for Cfg {
    fn default() -> Self {
        let version = match VERSION {
            Some(v) => v.to_string(),
            None => "".to_string(),
        };

        Cfg {
            version,
            arg_default: ArgDefault::default(),
            tmpl: Tmpl::default(),
            app_args: AppArgs::default(),
            clipboard: Clipboard::default(),
            filename: Filename::default(),
            viewer: Viewer::default(),
            exporter: Exporter::default(),
        }
    }
}

/// Default values for command line arguments.
impl ::std::default::Default for ArgDefault {
    fn default() -> Self {
        ArgDefault {
            debug: ARG_DEFAULT_DEBUG,
            edit: ARG_DEFAULT_EDITOR,
            no_filename_sync: ARG_DEFAULT_NO_FILENAME_SYNC,
            popup: ARG_DEFAULT_POPUP,
            tty: ARG_DEFAULT_TTY,
            add_header: ARG_DEFAULT_ADD_HEADER,
        }
    }
}

/// Default values for copy counter.
impl ::std::default::Default for Filename {
    fn default() -> Self {
        Filename {
            sort_tag_chars: FILENAME_SORT_TAG_CHARS.to_string(),
            sort_tag_extra_separator: FILENAME_SORT_TAG_EXTRA_SEPARATOR,
            copy_counter_extra_separator: FILENAME_COPY_COUNTER_EXTRA_SEPARATOR.to_string(),
            copy_counter_opening_brackets: FILENAME_COPY_COUNTER_OPENING_BRACKETS.to_string(),
            copy_counter_closing_brackets: FILENAME_COPY_COUNTER_CLOSING_BRACKETS.to_string(),
            extension_default: FILENAME_EXTENSION_DEFAULT.to_string(),
            extensions_md: FILENAME_EXTENSIONS_MD
                .iter()
                .map(|a| (*a).to_string())
                .collect(),
            extensions_rst: FILENAME_EXTENSIONS_RST
                .iter()
                .map(|a| (*a).to_string())
                .collect(),
            extensions_html: FILENAME_EXTENSIONS_HTML
                .iter()
                .map(|a| (*a).to_string())
                .collect(),
            extensions_txt: FILENAME_EXTENSIONS_TXT
                .iter()
                .map(|a| (*a).to_string())
                .collect(),
            extensions_no_viewer: FILENAME_EXTENSIONS_NO_VIEWER
                .iter()
                .map(|a| (*a).to_string())
                .collect(),
        }
    }
}

/// Default values for templates.
impl ::std::default::Default for Tmpl {
    fn default() -> Self {
        Tmpl {
            compulsory_header_field: TMPL_COMPULSORY_HEADER_FIELD.to_string(),
            new_content: TMPL_NEW_CONTENT.to_string(),
            new_filename: TMPL_NEW_FILENAME.to_string(),
            from_clipboard_yaml_content: TMPL_FROM_CLIPBOARD_YAML_CONTENT.to_string(),
            from_clipboard_yaml_filename: TMPL_FROM_CLIPBOARD_YAML_FILENAME.to_string(),
            from_clipboard_content: TMPL_FROM_CLIPBOARD_CONTENT.to_string(),
            from_clipboard_filename: TMPL_FROM_CLIPBOARD_FILENAME.to_string(),
            from_text_file_content: TMPL_FROM_TEXT_FILE_CONTENT.to_string(),
            from_text_file_filename: TMPL_FROM_TEXT_FILE_FILENAME.to_string(),
            annotate_file_content: TMPL_ANNOTATE_FILE_CONTENT.to_string(),
            annotate_file_filename: TMPL_ANNOTATE_FILE_FILENAME.to_string(),
            sync_filename: TMPL_SYNC_FILENAME.to_string(),
        }
    }
}

/// Default values for invoking external applications.
impl ::std::default::Default for AppArgs {
    fn default() -> Self {
        AppArgs {
            editor: APP_ARGS_EDITOR
                .iter()
                .map(|i| i.iter().map(|a| (*a).to_string()).collect())
                .collect(),
            editor_console: APP_ARGS_EDITOR_CONSOLE
                .iter()
                .map(|i| i.iter().map(|a| (*a).to_string()).collect())
                .collect(),
            browser: APP_ARGS_BROWSER
                .iter()
                .map(|i| i.iter().map(|a| (*a).to_string()).collect())
                .collect(),
        }
    }
}

/// Default values for clipboard behaviour.
impl ::std::default::Default for Clipboard {
    fn default() -> Self {
        Clipboard {
            read_enabled: CLIPBOARD_READ_ENABLED,
            empty_enabled: CLIPBOARD_EMPTY_ENABLED,
        }
    }
}

/// Default values for the viewer feature.
impl ::std::default::Default for Viewer {
    fn default() -> Self {
        Viewer {
            startup_delay: VIEWER_STARTUP_DELAY,
            missing_header_disables: VIEWER_MISSING_HEADER_DISABLES,
            notify_period: VIEWER_NOTIFY_PERIOD,
            tcp_connections_max: VIEWER_TCP_CONNECTIONS_MAX,
            served_mime_types: VIEWER_SERVED_MIME_TYPES
                .iter()
                .map(|i| i.iter().map(|a| (*a).to_string()).collect())
                .collect(),
            rendition_tmpl: VIEWER_RENDITION_TMPL.to_string(),
            error_tmpl: VIEWER_ERROR_TMPL.to_string(),
        }
    }
}

/// Default values for the exporter feature.
impl ::std::default::Default for Exporter {
    fn default() -> Self {
        Exporter {
            rendition_tmpl: EXPORTER_RENDITION_TMPL.to_string(),
        }
    }
}

lazy_static! {
    /// Store the extension as key and mime type as value in HashMap.
    pub static ref VIEWER_SERVED_MIME_TYPES_HMAP: HashMap<&'static str, &'static str> = {
        let mut hm = HashMap::new();
        for l in &CFG.viewer.served_mime_types {
            if l.len() >= 2
            {
                hm.insert(l[0].as_str(), l[1].as_str());
            };
        };
        hm
    };
}

lazy_static! {
    /// Variable indicating with `Err` if the loading of the configuration file went wrong.
    pub static ref CFG_FILE_LOADING: RwLock<Result<(), FileError>> = RwLock::new(Ok(()));
}

/// Parse the configuration file if it exists. Otherwise write one with default values.
#[cfg(not(test))]
#[inline]
fn config_load(config_path: &Path) -> Result<Cfg, FileError> {
    if config_path.exists() {
        let config: Cfg = toml::from_str(&fs::read_to_string(config_path)?)?;
        // Check for obvious configuration errors.
        if config
            .filename
            .sort_tag_chars
            .find(config.filename.sort_tag_extra_separator)
            .is_some()
            || config.filename.sort_tag_extra_separator == FILENAME_DOTFILE_MARKER
        {
            return Err(FileError::ConfigFileSortTag {
                char: FILENAME_DOTFILE_MARKER,
                chars: config.filename.sort_tag_chars.escape_default().to_string(),
                extra_separator: config
                    .filename
                    .sort_tag_extra_separator
                    .escape_default()
                    .to_string(),
            });
        }

        // Check for obvious configuration errors.
        if !TRIM_LINE_CHARS.contains(&config.filename.copy_counter_extra_separator) {
            return Err(FileError::ConfigFileCopyCounter {
                chars: TRIM_LINE_CHARS.escape_default().to_string(),
                extra_separator: config
                    .filename
                    .copy_counter_extra_separator
                    .escape_default()
                    .to_string(),
            });
        }
        // First check passed.
        Ok(config)
    } else {
        let cfg = Cfg::default();
        config_write(&cfg, config_path)?;
        Ok(cfg)
    }
}

/// In unit tests we use the default configuration values.
#[cfg(test)]
#[inline]
fn config_load(_config_path: &Path) -> Result<Cfg, FileError> {
    Ok(Cfg::default())
}

/// Writes the default configuration to `Path`.
#[cfg(not(test))]
fn config_write(config: &Cfg, config_path: &Path) -> Result<(), FileError> {
    fs::create_dir_all(config_path.parent().unwrap_or_else(|| Path::new("")))?;

    let mut buffer = File::create(config_path)?;
    buffer.write_all(toml::to_string_pretty(config)?.as_bytes())?;
    Ok(())
}

/// In unit tests we do not write anything.
#[cfg(test)]
fn config_write(_config: &Cfg, _config_path: &Path) -> Result<(), FileError> {
    Ok(())
}

lazy_static! {
    /// Reads and parses the configuration file "tp-note.toml". An alternative
    /// filename (optionally with absolute path) can be given on the command line
    /// with "--config".
    pub static ref CFG: Cfg = {
        let config_path = if let Some(c) = &ARGS.config {
            Path::new(c)
        } else {
            match &*CONFIG_PATH {
                Some(p) => p.as_path(),
                None => {
                    // Remember that something went wrong.
                    let mut cfg_file_loading = CFG_FILE_LOADING.write().unwrap();
                    *cfg_file_loading = Err(FileError::PathToConfigFileNotFound);
                    return Cfg::default();
                },
            }
        };

        config_load(config_path)
            .unwrap_or_else(|e|{
                // Remember that something went wrong.
                let mut cfg_file_loading = CFG_FILE_LOADING.write().unwrap();
                *cfg_file_loading = Err(e);

                // As we could not load the config file, we will use the default
                // configuration.
                Cfg::default()
            })
        };
}

lazy_static! {
/// This is where the Tp-Note stores its configuration file.
    pub static ref CONFIG_PATH : Option<PathBuf> = {
        if let Some(c) = &ARGS.config {
            Some(PathBuf::from(c))
        } else {
            let config = ProjectDirs::from("rs", "", CURRENT_EXE)?;

            let mut config = PathBuf::from(config.config_dir());
            config.push(Path::new(CONFIG_FILENAME));
            Some(config)
        }
    };
}

pub fn backup_config_file() -> Result<PathBuf, FileError> {
    if let Some(ref config_path) = *CONFIG_PATH {
        if config_path.exists() {
            let config_path_bak = filename::find_unused((config_path).to_path_buf())?;

            fs::rename(&config_path.as_path(), &config_path_bak)?;

            config_write(&Cfg::default(), config_path)?;

            Ok(config_path_bak)
        } else {
            Err(FileError::ConfigFileNotFound)
        }
    } else {
        Err(FileError::PathToConfigFileNotFound)
    }
}