├── _project.vim
├── res
├── icon.png
├── help_popup.html
├── style
│ ├── main.css
│ └── github.css
├── layout.html
├── default_config.yaml
└── js
│ └── main.js
├── .gitignore
├── src
├── lib.rs
├── main.rs
├── ui
│ ├── dialogs.rs
│ ├── browser.rs
│ ├── action.rs
│ └── mod.rs
├── markdown.rs
├── background.rs
├── assets.rs
└── input.rs
├── CHANGELOG.md
├── .github
└── workflows
│ └── release-binary.yml
├── LICENSE-MIT
├── Cargo.toml
├── tests
├── test_input.rs
├── test_markdown.rs
├── test_background.rs
├── test_assets.rs
└── test_ui_actions.rs
├── CODE_OF_CONDUCT.md
├── README.md
├── LICENSE-APACHE
└── Cargo.lock
/_project.vim:
--------------------------------------------------------------------------------
1 | runtime! projects/rust.vim
2 |
--------------------------------------------------------------------------------
/res/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AndrewRadev/quickmd/HEAD/res/icon.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | **/*.rs.bk
3 | /.cargo/config.toml
4 |
5 | notes
6 |
7 | # Ignored since it contains system-specific linker config
8 | /.cargo/config.toml
9 |
--------------------------------------------------------------------------------
/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![warn(missing_docs)]
2 |
3 | //! This is a GTK app that allows you to quickly preview a markdown file. It's not intended to be
4 | //! used as a library, but the individual components are there, and theoretically usable. Check the
5 | //! `README.md` file for instructions on how to use it as an app.
6 |
7 | pub mod assets;
8 | pub mod background;
9 | pub mod input;
10 | pub mod markdown;
11 | pub mod ui;
12 |
--------------------------------------------------------------------------------
/res/help_popup.html:
--------------------------------------------------------------------------------
1 |
2 | Configuration
3 |
4 | Settings: {yaml_path}
5 | CSS: {css_path}
6 |
7 | Default Keybindings
8 |
9 | j/k: Scroll up and down.
10 | J/K: Scroll up and down by a larger amount.
11 | g/G: Jump to the beginning/end of the document.
12 |
13 | e: Launch an external editor (needs to be configured).
14 | E: Launch an external editor and exit viewer.
15 |
16 | CTRL + <scroll>: Zoom in/out
17 | +/-/=: Zoom in/out/reset
18 |
19 | CTRL + q: Quit
20 |
--------------------------------------------------------------------------------
/res/style/main.css:
--------------------------------------------------------------------------------
1 | main {
2 | width: 66%;
3 | margin: 0 auto;
4 | }
5 |
6 | #link-preview {
7 | position: fixed;
8 | bottom: 0;
9 | right: 0;
10 | padding: 5px 10px;
11 | background-color: #f5f6f7;
12 | border-radius: 5px 0 0 0;
13 | border-top: 1px solid #dcdfe3;
14 | border-left: 1px solid #dcdfe3;
15 | font-size: 0.7em;
16 | opacity: 0;
17 | }
18 |
19 | #link-preview.visible {
20 | opacity: 0.95;
21 | transition: 300ms ease-out;
22 | }
23 |
24 | #link-preview.hidden {
25 | opacity: 0;
26 | transition: 300ms ease-out;
27 | }
28 |
--------------------------------------------------------------------------------
/res/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {page_state}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {body}
16 |
17 |
18 |
19 |
20 |
21 |
23 |
24 | {hl_tags}
25 |
26 |
27 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | ## [0.3.0] - 2020-05-06
6 |
7 | - Added: Code highlighting
8 | - Added: GFM features like tables and strikethrough (basically, whatever [pulldown-cmark provides](https://docs.rs/pulldown-cmark/0.7.0/pulldown_cmark/struct.Options.html))
9 |
10 | ## [0.2.0] - 2020-04-05
11 |
12 | - Added: Image sizes are cached via javascript and set on page reload. That way, reloading the
13 | browser due to text changes should not lead to any flicker.
14 | - Added: Reading markdown from STDIN is now possible by providing `-` as a filename.
15 | - Changed: Custom header bar was removed. One should be added automatically by the window
16 | manager/desktop environment.
17 |
18 | ## [0.1.2] - 2020-03-23
19 |
20 | - Fixed: file-watching bug
21 |
22 | ## [0.1.1] - 2020-03-22
23 |
24 | - Initial release
25 |
--------------------------------------------------------------------------------
/.github/workflows/release-binary.yml:
--------------------------------------------------------------------------------
1 | name: Release binary
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-22.04
8 |
9 | steps:
10 | - name: Checkout
11 | uses: actions/checkout@v1
12 |
13 | - name: Install latest rust toolchain
14 | uses: actions-rs/toolchain@v1
15 | with:
16 | toolchain: stable
17 | default: true
18 | override: true
19 |
20 | - name: Install gtk and webkit2gtk
21 | run: sudo apt update && sudo apt install libgtk-3-dev libwebkit2gtk-4.0-dev
22 |
23 | - name: Build
24 | run: cargo build --all --release && strip target/release/quickmd && mv target/release/quickmd target/release/quickmd_amd64
25 |
26 | - name: Release
27 | uses: softprops/action-gh-release@v1
28 | if: startsWith(github.ref, 'refs/tags/')
29 | with:
30 | files: |
31 | target/release/quickmd_amd64
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 Andrew Radev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9 | of the Software, and to permit persons to whom the Software is furnished to do
10 | so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "quickmd"
3 | version = "0.7.1"
4 | authors = ["Andrew Radev "]
5 | edition = "2024"
6 | license = "MIT OR Apache-2.0"
7 | description = "Quickly preview a markdown file"
8 | readme = "README.md"
9 | keywords = ["markdown", "gtk"]
10 | exclude = ["tests/**", "_project.vim"]
11 | repository = "https://github.com/AndrewRadev/quickmd"
12 |
13 | [badges]
14 | maintenance = { status = "actively-developed" }
15 |
16 | [dependencies]
17 | anyhow = "1.0.68"
18 | directories = "4.0.1"
19 | env_logger = { version = "0.10.0", default-features = false }
20 | gdk = "0.15"
21 | gdk-pixbuf = "0.15"
22 | gio = "0.15"
23 | glib = "0.15"
24 | gtk = { version = "0.15", features = ["v3_24"] }
25 | log = "0.4"
26 | notify = "4.0.17"
27 | pathbuftools = "0.1.2"
28 | pulldown-cmark = { version = "0.9.2", default-features = false, features = ["simd"] }
29 | regex = "1.7.1"
30 | serde = { version = "1.0", features = ["derive"] }
31 | serde_json = "1.0"
32 | serde_yaml = "0.9.16"
33 | structopt = { version = "0.3.26", default-features = false }
34 | tempfile = "3.3.0"
35 | webkit2gtk = "0.18.2"
36 |
37 | [dev-dependencies]
38 | claim = "0.5.0"
39 |
--------------------------------------------------------------------------------
/res/default_config.yaml:
--------------------------------------------------------------------------------
1 | # The default zoom level of the webview. In case of a HiDPI display, it can be useful to set this
2 | # to a higher value.
3 | #
4 | # In the UI, the zoom level can be changed in increments of 0.1 by holding the Control key and
5 | # scrolling up or down.
6 | #
7 | zoom: 1.0
8 |
9 | # The command-line components to launch an editor with the "e" key (or exec into one with "E"). The
10 | # `{path}` placeholder will be replaced with the full path to the current markdown file.
11 | #
12 | # Other examples for editor command-lines:
13 | #
14 | # - ["code", "{path}"]
15 | # - ["gnome-terminal", "--", "vim", "{path}"]
16 | # - ["alacritty", "-e", "vim", "{path}"]
17 | #
18 | editor_command: ["gvim", "{path}"]
19 |
20 | # You can set your own keybindings, or unset the defaults by setting them to
21 | # "Noop". See the API documentaiton for a full list of actions, under
22 | # `ui::action::Action`.
23 | #
24 | # The keybindings are passed along to GDK:
25 | # - if given `key_char`, it's passed along to `Key::from_unicode`
26 | # - if given `key_name`, `Key::from_name` is called with it. So, "plus"
27 | # instead of "+". You can also use it for special keys like "Escape", "F1", etc.
28 | # - Modifiers that are supported: "control", "shift", "alt"
29 | #
30 | mappings: []
31 | # mappings:
32 | # - { key_char: "q", action: "Quit" }
33 | # - { key_name: "minus", mods: ["control"], action: "ZoomOut" }
34 |
--------------------------------------------------------------------------------
/tests/test_input.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 | use std::fs;
3 |
4 | use claim::assert_matches;
5 | use quickmd::input::InputFile;
6 |
7 | #[test]
8 | fn test_input_file_constructed_from_local_file() {
9 | let path = PathBuf::from("./test.md");
10 | let buffer = Vec::::new();
11 | let input_file = InputFile::from(&path, buffer.as_slice()).unwrap();
12 |
13 | assert_matches!(input_file, InputFile::Filesystem(_));
14 | assert_eq!("./test.md", input_file.path().to_str().unwrap());
15 | }
16 |
17 | #[test]
18 | fn test_input_file_constructed_from_stdin() {
19 | let path = PathBuf::from("-");
20 | let buffer = Vec::::new();
21 | let input_file = InputFile::from(&path, buffer.as_slice()).unwrap();
22 |
23 | assert_matches!(input_file, InputFile::Stdin(_));
24 | // Not really "-", would be a temporary file:
25 | assert_ne!("-", input_file.path().to_str().unwrap());
26 | }
27 |
28 | #[test]
29 | fn test_reading_from_file_built_from_stdin() {
30 | let path = PathBuf::from("-");
31 | let buffer: Vec = "# Test\nTest content".bytes().collect();
32 | let input_file = InputFile::from(&path, buffer.as_slice()).unwrap();
33 |
34 | assert_eq!(b"# Test\nTest content", fs::read(input_file.path()).unwrap().as_slice());
35 | }
36 |
37 | #[test]
38 | fn test_temporary_file_is_cleaned_up_on_drop() {
39 | let path = PathBuf::from("-");
40 | let buffer: Vec = "# Test\nTest content".bytes().collect();
41 | let input_file = InputFile::from(&path, buffer.as_slice()).unwrap();
42 | let path = input_file.path().to_path_buf();
43 |
44 | assert!(path.exists());
45 | drop(input_file);
46 | assert!(!path.exists());
47 | }
48 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 | use std::process;
3 | use std::path::{Path, PathBuf};
4 |
5 | use anyhow::anyhow;
6 | use log::debug;
7 |
8 | use quickmd::assets::Assets;
9 | use quickmd::background;
10 | use quickmd::input::{Config, Options, InputFile};
11 | use quickmd::markdown::Renderer;
12 | use quickmd::ui;
13 |
14 | fn main() {
15 | let options = Options::build();
16 | options.init_logging();
17 |
18 | let config = Config::load().
19 | unwrap_or_default();
20 |
21 | debug!("Loaded config: {:?}", config);
22 | debug!(" > path: {}", Config::yaml_path().display());
23 | debug!("Using input options: {:?}", options);
24 |
25 | if let Err(e) = run(&config, &options) {
26 | eprintln!("{}", e);
27 | process::exit(1);
28 | }
29 | }
30 |
31 | fn run(config: &Config, options: &Options) -> anyhow::Result<()> {
32 | if options.install_default_config {
33 | return Config::try_install_default();
34 | }
35 |
36 | gtk::init()?;
37 |
38 | if let Some(input_file) = options.input_file.as_ref() {
39 | launch_app(input_file, options, config)
40 | } else {
41 | let input_file = launch_file_picker()?;
42 | launch_app(&input_file, options, config)
43 | }
44 | }
45 |
46 | fn launch_file_picker() -> anyhow::Result {
47 | ui::dialogs::FilePicker::new().run().ok_or_else(|| {
48 | anyhow!("Please provide a markdown file to render or call the program with - to read from STDIN")
49 | })
50 | }
51 |
52 | fn launch_app(input_file: &Path, options: &Options, config: &Config) -> anyhow::Result<()> {
53 | let input_file = InputFile::from(input_file, io::stdin())?;
54 | let is_real_file = input_file.is_real_file();
55 | let md_path = input_file.path();
56 |
57 | if !md_path.exists() {
58 | let error = anyhow!("File not found: {}", md_path.display());
59 | return Err(error);
60 | }
61 |
62 | let renderer = Renderer::new(md_path.to_path_buf());
63 | let assets = Assets::init(options.output_dir.clone())?;
64 |
65 | let mut ui = ui::App::init(config.clone(), input_file.clone(), assets)?;
66 | let (ui_sender, ui_receiver) = glib::MainContext::channel(glib::PRIORITY_DEFAULT);
67 | ui.init_render_loop(ui_receiver);
68 |
69 | // Initial render
70 | ui_sender.send(ui::Event::LoadHtml(renderer.run()?))?;
71 |
72 | if is_real_file && options.watch {
73 | background::init_update_loop(renderer, ui_sender);
74 | }
75 |
76 | ui.run();
77 | Ok(())
78 | }
79 |
--------------------------------------------------------------------------------
/src/ui/dialogs.rs:
--------------------------------------------------------------------------------
1 | //! Modal dialogs.
2 |
3 | use std::path::PathBuf;
4 |
5 | use gtk::prelude::*;
6 |
7 | use crate::input::Config;
8 |
9 | /// A popup to choose a file if it wasn't provided on the command-line.
10 | ///
11 | pub struct FilePicker(gtk::FileChooserDialog);
12 |
13 | impl FilePicker {
14 | /// Construct a new file picker that only shows markdown files by default
15 | ///
16 | pub fn new() -> FilePicker {
17 | let dialog = gtk::FileChooserDialog::new(
18 | Some("Open"),
19 | Some(>k::Window::new(gtk::WindowType::Popup)),
20 | gtk::FileChooserAction::Open,
21 | );
22 |
23 | // Only show markdown files
24 | let filter = gtk::FileFilter::new();
25 | filter.set_name(Some("Markdown files (*.md, *.markdown)"));
26 | filter.add_pattern("*.md");
27 | filter.add_pattern("*.markdown");
28 | dialog.add_filter(&filter);
29 |
30 | // Just in case, allow showing all files
31 | let filter = gtk::FileFilter::new();
32 | filter.add_pattern("*");
33 | filter.set_name(Some("All files"));
34 | dialog.add_filter(&filter);
35 |
36 | // Add the cancel and open buttons to that dialog.
37 | dialog.add_button("Cancel", gtk::ResponseType::Cancel);
38 | dialog.add_button("Open", gtk::ResponseType::Ok);
39 |
40 | FilePicker(dialog)
41 | }
42 |
43 | /// Open the file picker popup and get the selected file.
44 | ///
45 | pub fn run(&self) -> Option {
46 | if self.0.run() == gtk::ResponseType::Ok {
47 | self.0.filename()
48 | } else {
49 | None
50 | }
51 | }
52 | }
53 |
54 | impl Drop for FilePicker {
55 | fn drop(&mut self) { self.0.close(); }
56 | }
57 |
58 | /// Open a popup that renders documentation for all the default keyboard and mouse mappings.
59 | ///
60 | pub fn open_help_dialog(window: >k::Window) -> gtk::ResponseType {
61 | use gtk::{DialogFlags, MessageType, ButtonsType};
62 |
63 | let dialog = gtk::MessageDialog::new(
64 | Some(window),
65 | DialogFlags::MODAL | DialogFlags::DESTROY_WITH_PARENT,
66 | MessageType::Info,
67 | ButtonsType::Close,
68 | ""
69 | );
70 |
71 | let content = format!{
72 | include_str!("../../res/help_popup.html"),
73 | yaml_path = Config::yaml_path().display(),
74 | css_path = Config::css_path().display(),
75 | };
76 |
77 | dialog.set_markup(&content);
78 | dialog.connect_response(|d, _response| d.close());
79 |
80 | dialog.run()
81 | }
82 |
--------------------------------------------------------------------------------
/tests/test_markdown.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashSet;
2 | use std::io::Write;
3 |
4 | use tempfile::NamedTempFile;
5 | use quickmd::markdown::Renderer;
6 |
7 | #[test]
8 | fn test_keeps_track_of_rendered_languages() {
9 | let mut file = NamedTempFile::new().unwrap();
10 |
11 | writeln!(file, "``` vim" ).unwrap();
12 | writeln!(file, "```" ).unwrap();
13 | writeln!(file, "" ).unwrap();
14 | writeln!(file, " ignored" ).unwrap();
15 | writeln!(file, "" ).unwrap();
16 | writeln!(file, "```ruby" ).unwrap();
17 | writeln!(file, "```" ).unwrap();
18 | writeln!(file, "" ).unwrap();
19 | writeln!(file, "```" ).unwrap();
20 | writeln!(file, "```" ).unwrap();
21 | writeln!(file, "``` rust").unwrap();
22 | writeln!(file, "```" ).unwrap();
23 |
24 | let renderer = Renderer::new(file.path().to_path_buf());
25 | let content = renderer.run().unwrap();
26 | let expected: HashSet<_> =
27 | vec!["vim", "ruby", "rust"].into_iter().map(String::from).collect();
28 |
29 | assert_eq!(expected, content.code_languages);
30 | }
31 |
32 | #[test]
33 | fn test_renders_local_images() {
34 | let mut file = NamedTempFile::new().unwrap();
35 | let tempdir = file.path().parent().unwrap().to_path_buf();
36 |
37 | writeln!(file, "").unwrap();
38 | writeln!(file, "").unwrap();
39 | writeln!(file, "").unwrap();
40 | writeln!(file, "").unwrap();
41 | writeln!(file, "").unwrap();
42 | writeln!(file, "").unwrap();
43 | writeln!(file, "").unwrap();
44 |
45 | let renderer = Renderer::new(file.path().to_path_buf());
46 | let content = renderer.run().unwrap();
47 |
48 | assert!(content.html.contains(&format!("src=\"file://{}/local-image-01.png\"", tempdir.display())));
49 | assert!(content.html.contains(&format!("src=\"file://{}/.local-image-02.png\"", tempdir.display())));
50 | assert!(content.html.contains(&format!("src=\"file://{}/local-image-03.png\"", tempdir.display())));
51 | assert!(content.html.contains(&format!("src=\"file://{}/local-image-04.png\"", tempdir.display())));
52 | assert!(content.html.contains(&format!("src=\"file://{}/../local-image-05.png\"", tempdir.display())));
53 | assert!(content.html.contains("src=\"http://remote-image-01.png\""));
54 | assert!(content.html.contains("src=\"https://remote-image-02.png\""));
55 | }
56 |
--------------------------------------------------------------------------------
/tests/test_background.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::sync::mpsc;
3 | use std::sync::mpsc::RecvTimeoutError::Timeout as TimeoutError;
4 | use std::time::Duration;
5 |
6 | use claim::assert_matches;
7 |
8 | use quickmd::ui;
9 | use quickmd::markdown::Renderer;
10 | use quickmd::background::init_update_loop;
11 |
12 | #[test]
13 | fn test_update_loop_detects_file_updates() {
14 | let tempdir = tempfile::tempdir().unwrap();
15 | let path = tempdir.path().join("file.md");
16 |
17 | fs::write(&path, "# Test").unwrap();
18 | let renderer = Renderer::new(path.clone());
19 |
20 | let (sender, receiver) = mpsc::channel();
21 | init_update_loop(renderer, sender);
22 | // Wait for the watcher thread to get ready
23 | std::thread::sleep(Duration::from_millis(10));
24 |
25 | fs::write(path, "# Changed").unwrap();
26 |
27 | // Expect LoadHtml message
28 | let message = receiver.recv_timeout(Duration::from_millis(300));
29 | assert_matches!(message, Ok(ui::Event::LoadHtml(_)));
30 |
31 | // Expect no further message
32 | let message = receiver.recv_timeout(Duration::from_millis(300));
33 | assert_matches!(message, Err(TimeoutError));
34 | }
35 |
36 | #[test]
37 | fn test_update_loop_detects_file_creation() {
38 | let tempdir = tempfile::tempdir().unwrap();
39 | let path = tempdir.path().join("file.md");
40 |
41 | fs::write(&path, "# Test").unwrap();
42 | let renderer = Renderer::new(path.clone());
43 |
44 | let (sender, receiver) = mpsc::channel();
45 | init_update_loop(renderer, sender);
46 | // Wait for the watcher thread to get ready
47 | std::thread::sleep(Duration::from_millis(10));
48 |
49 | fs::remove_file(&path).unwrap();
50 | fs::write(&path, "# Changed").unwrap();
51 |
52 | // Expect LoadHtml message
53 | let message = receiver.recv_timeout(Duration::from_millis(300));
54 | assert_matches!(message, Ok(ui::Event::LoadHtml(_)));
55 |
56 | // Expect no further message
57 | let message = receiver.recv_timeout(Duration::from_millis(300));
58 | assert_matches!(message, Err(TimeoutError));
59 | }
60 |
61 | #[test]
62 | fn test_update_loop_ignores_unrelated_files() {
63 | let tempdir = tempfile::tempdir().unwrap();
64 | let path = tempdir.path().join("file.md");
65 | let other_path = tempdir.path().join("other.md");
66 |
67 | // Create both files
68 | fs::write(&path, "# Test").unwrap();
69 | fs::write(&other_path, "# Other").unwrap();
70 |
71 | let renderer = Renderer::new(path.clone());
72 |
73 | let (sender, receiver) = mpsc::channel();
74 | init_update_loop(renderer, sender);
75 | // Wait for the watcher thread to get ready
76 | std::thread::sleep(Duration::from_millis(10));
77 |
78 | // Update only the "other" file
79 | fs::write(&other_path, "# Updated").unwrap();
80 |
81 | // No message
82 | let message = receiver.recv_timeout(Duration::from_millis(300));
83 | assert_matches!(message, Err(TimeoutError));
84 | }
85 |
--------------------------------------------------------------------------------
/res/js/main.js:
--------------------------------------------------------------------------------
1 | // Page state container
2 | let title = document.querySelector('title');
3 | // Page state object
4 | let page_state = JSON.parse(title.innerHTML);
5 |
6 | // Update scroll position on load:
7 | window.scroll(0, page_state.scroll_top);
8 |
9 | // Store scroll position on scroll:
10 | window.addEventListener('scroll', function() {
11 | page_state.scroll_top = window.pageYOffset;
12 | title.innerHTML = JSON.stringify(page_state);
13 | });
14 |
15 | // Set image sizes we have data for, store sizes for new images:
16 | document.querySelectorAll('img').forEach(function(img) {
17 | const width = page_state.image_widths[img.src];
18 | const height = page_state.image_heights[img.src];
19 | let style = "";
20 |
21 | if (width) { style = `${style} width: ${width}px;`; }
22 | if (height) { style = `${style} height: ${height}px;`; }
23 |
24 | img.style = style;
25 |
26 | img.onload = function() {
27 | // Remove the style overloads on load in case the image has changed:
28 | img.style = "";
29 |
30 | // Cache calculated sizes:
31 | page_state.image_heights[this.src] = this.height;
32 | page_state.image_widths[this.src] = this.width;
33 | title.innerHTML = JSON.stringify(page_state);
34 | };
35 | });
36 |
37 | // Create anchors for all the headings:
38 | document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(function(heading) {
39 | if (!heading.id) {
40 | let content = heading.innerHTML.trim().toLowerCase();
41 | let slug = content.replace(/[^\w\d]+/g, '-');
42 |
43 | heading.id = slug;
44 | }
45 | });
46 |
47 | // Show link preview at the bottom:
48 | let linkPreview = document.querySelector('#link-preview');
49 | let rootUrl = window.location.href;
50 |
51 | document.querySelectorAll('a').forEach(function(link) {
52 | let url = link.href;
53 | let description;
54 |
55 | if (url.startsWith(rootUrl)) {
56 | // it's a local anchor, let's just take that part
57 | description = `Jump: ${url.replace(rootUrl, '')}`
58 | } else {
59 | // it's an external URL, copy it
60 | description = `Copy: ${url}`
61 |
62 | link.addEventListener('click', function(e) {
63 | e.preventDefault();
64 |
65 | // Create a temporary text input whose contents we can "select" and copy using `execCommand`.
66 | let tempInput = document.createElement('input');
67 | tempInput.setAttribute('type', 'text');
68 | tempInput.setAttribute('value', url);
69 | document.body.insertBefore(tempInput, linkPreview);
70 |
71 | tempInput.select();
72 | document.execCommand('copy');
73 | document.body.removeChild(tempInput);
74 |
75 | linkPreview.innerHTML = "Copied to clipboard!";
76 | });
77 | }
78 |
79 | link.addEventListener('mouseenter', function() {
80 | linkPreview.innerHTML = description;
81 | linkPreview.classList.remove('hidden');
82 | linkPreview.classList.add('visible');
83 | });
84 |
85 | link.addEventListener('mouseleave', function() {
86 | linkPreview.classList.remove('visible');
87 | linkPreview.classList.add('hidden');
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/src/markdown.rs:
--------------------------------------------------------------------------------
1 | //! Markdown rendering.
2 | //!
3 | //! Uses the [`pulldown_cmark`] crate with Github-flavored markdown options enabled. Extracts
4 | //! languages used in code blocks for highlighting purposes.
5 |
6 | use std::fs;
7 | use std::io;
8 | use std::path::{PathBuf, Path};
9 | use std::collections::HashSet;
10 | use regex::Regex;
11 | use pulldown_cmark::{Parser, Options, Event, html};
12 |
13 | /// Encapsulates a markdown file and provides an interface to turn its contents into HTML.
14 | ///
15 | pub struct Renderer {
16 | /// The original path given to the renderer.
17 | pub md_path: PathBuf,
18 |
19 | /// The canonicalized path to use in file operations.
20 | pub canonical_md_path: PathBuf,
21 | }
22 |
23 | impl Renderer {
24 | /// Create a new renderer instance that wraps the given markdown file.
25 | ///
26 | pub fn new(md_path: PathBuf) -> Self {
27 | let canonical_md_path = md_path.canonicalize().
28 | unwrap_or_else(|_| md_path.clone());
29 |
30 | Renderer { md_path, canonical_md_path }
31 | }
32 |
33 | /// Turn the current contents of the markdown file into HTML.
34 | ///
35 | pub fn run(&self) -> Result {
36 | let markdown = fs::read_to_string(&self.canonical_md_path)?;
37 | let root_dir = self.canonical_md_path.parent().unwrap_or_else(|| Path::new(""));
38 |
39 | let re_absolute_url = Regex::new(r"^[a-z]+://").unwrap();
40 | let re_path_prefix = Regex::new(r"^(/|\./)?").unwrap();
41 |
42 | let mut options = Options::empty();
43 | options.insert(Options::ENABLE_TABLES);
44 | options.insert(Options::ENABLE_FOOTNOTES);
45 | options.insert(Options::ENABLE_TASKLISTS);
46 | options.insert(Options::ENABLE_STRIKETHROUGH);
47 | let parser = Parser::new_ext(&markdown, options);
48 |
49 | let mut languages = HashSet::new();
50 | let parser = parser.map(|mut event| {
51 | use pulldown_cmark::{Tag, CodeBlockKind};
52 |
53 | match &mut event {
54 | Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(content))) => {
55 | if content.len() > 0 {
56 | languages.insert(content.to_string());
57 | }
58 | },
59 | Event::Start(Tag::Image(_, url, _)) if !re_absolute_url.is_match(url) => {
60 | *url = format!("file://{}/{}", root_dir.display(), re_path_prefix.replace(url, "")).into();
61 | },
62 | _ => (),
63 | }
64 |
65 | event
66 | });
67 |
68 | let mut output = String::new();
69 | html::push_html(&mut output, parser);
70 |
71 | Ok(RenderedContent {
72 | html: output,
73 | code_languages: languages,
74 | })
75 | }
76 | }
77 |
78 | /// The output of the rendering process. Includes both the rendered HTML and additional metadata
79 | /// used by its clients.
80 | ///
81 | #[derive(Debug, Default)]
82 | pub struct RenderedContent {
83 | /// The rendered HTML.
84 | pub html: String,
85 |
86 | /// All the languages in fenced code blocks from the markdown input.
87 | pub code_languages: HashSet,
88 | }
89 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at andrey.radev@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/src/ui/browser.rs:
--------------------------------------------------------------------------------
1 | //! A container for the `Browser` struct that wraps the [`webkit2gtk::WebView`].
2 |
3 | use std::time::Instant;
4 |
5 | use anyhow::anyhow;
6 | use gio::Cancellable;
7 | use gtk::prelude::*;
8 | use log::{debug, warn};
9 | use webkit2gtk::traits::WebViewExt;
10 | use webkit2gtk::{WebContext, WebView};
11 |
12 | use crate::assets::PageState;
13 | use crate::input::Config;
14 |
15 | /// A thin layer on top of [`webkit2gtk::WebView`] to put helper methods into.
16 | ///
17 | #[derive(Clone)]
18 | pub struct Browser {
19 | webview: WebView,
20 | config: Config,
21 | }
22 |
23 | impl Browser {
24 | /// Construct a new instance with the provided `Config`.
25 | ///
26 | pub fn new(config: Config) -> anyhow::Result {
27 | let web_context = WebContext::default().
28 | ok_or_else(|| anyhow!("Couldn't initialize GTK WebContext"))?;
29 | let webview = WebView::with_context(&web_context);
30 | webview.set_zoom_level(config.zoom);
31 |
32 | Ok(Browser { webview, config })
33 | }
34 |
35 | /// Add this browser instance's webview to the given GTK Window.
36 | ///
37 | pub fn attach_to(&self, window: >k::Window) {
38 | window.add(&self.webview);
39 | }
40 |
41 | /// Delegates to [`webkit2gtk::WebView`]
42 | pub fn load_uri(&self, uri: &str) {
43 | self.webview.load_uri(uri);
44 | }
45 |
46 | /// Delegates to [`webkit2gtk::WebView`]
47 | pub fn reload(&self) {
48 | self.webview.reload();
49 | }
50 |
51 | /// Increase zoom level by ~10%
52 | ///
53 | pub fn zoom_in(&self) {
54 | let zoom_level = self.webview.zoom_level();
55 | self.webview.set_zoom_level(zoom_level + 0.1);
56 | debug!("Zoom level set to: {}", zoom_level);
57 | }
58 |
59 | /// Decrease zoom level by ~10%, down until 20% or so.
60 | ///
61 | pub fn zoom_out(&self) {
62 | let zoom_level = self.webview.zoom_level();
63 |
64 | if zoom_level > 0.2 {
65 | self.webview.set_zoom_level(zoom_level - 0.1);
66 | debug!("Zoom level set to: {}", zoom_level);
67 | }
68 | }
69 |
70 | /// Reset to the base zoom level defined in the config (which defaults to 100%).
71 | ///
72 | pub fn zoom_reset(&self) {
73 | self.webview.set_zoom_level(self.config.zoom);
74 | debug!("Zoom level set to: {}", self.config.zoom);
75 | }
76 |
77 | /// Get the deserialized `PageState` from the current contents of the webview. This is later
78 | /// rendered unchanged into the HTML content.
79 | ///
80 | pub fn get_page_state(&self) -> PageState {
81 | match self.webview.title() {
82 | Some(t) => {
83 | serde_json::from_str(t.as_str()).unwrap_or_else(|e| {
84 | warn!("Failed to get page state from {}: {:?}", t, e);
85 | PageState::default()
86 | })
87 | },
88 | None => PageState::default(),
89 | }
90 | }
91 |
92 | /// Execute some (async) javascript code in the webview, without checking the result other than
93 | /// printing a warning if it errors out.
94 | ///
95 | pub fn execute_js(&self, js_code: &'static str) {
96 | let now = Instant::now();
97 |
98 | self.webview.run_javascript(js_code, None::<&Cancellable>, move |result| {
99 | if let Err(e) = result {
100 | warn!("Javascript execution error: {}", e);
101 | } else {
102 | debug!("Javascript executed in {}ms:\n> {}", now.elapsed().as_millis(), js_code);
103 | }
104 | });
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/test_assets.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use claim::assert_matches;
3 |
4 | use quickmd::assets::{Assets, PageState};
5 | use quickmd::markdown::RenderedContent;
6 |
7 | macro_rules! assert_contains {
8 | ($haystack:expr, $needle:expr) => {
9 | if $haystack.contains($needle) {
10 | assert!(true);
11 | } else {
12 | assert!(false, "\n\nExpected:\n\n{}\nto contain:\n\n{}\n\n", $haystack, $needle)
13 | }
14 | }
15 | }
16 |
17 | #[test]
18 | fn test_initialization_works() {
19 | Assets::init(None).unwrap();
20 | }
21 |
22 | #[test]
23 | fn test_multiple_cleanups_work() {
24 | let mut assets = Assets::init(None).unwrap();
25 |
26 | assets.clean_up();
27 | assets.clean_up();
28 | }
29 |
30 | #[test]
31 | fn test_building_a_file_with_assets_includes_the_given_html() {
32 | let assets = Assets::init(None).unwrap();
33 | let content = RenderedContent {
34 | html: String::from("Example
"),
35 | ..RenderedContent::default()
36 | };
37 |
38 | let path = assets.build(&content, &PageState::default()).unwrap();
39 |
40 | assert!(fs::read_to_string(&path).unwrap().contains(&content.html));
41 | assert!(fs::read_to_string(&path).unwrap().contains("main.js"));
42 | assert!(fs::read_to_string(&path).unwrap().contains("main.css"));
43 | }
44 |
45 | #[test]
46 | fn test_building_a_file_with_assets_includes_main_static_files() {
47 | let assets = Assets::init(None).unwrap();
48 | let path = assets.build(&RenderedContent::default(), &PageState::default()).unwrap();
49 |
50 | assert!(fs::read_to_string(&path).unwrap().contains("main.js"));
51 | assert!(fs::read_to_string(&path).unwrap().contains("main.css"));
52 | }
53 |
54 | #[test]
55 | fn test_building_a_file_with_assets_includes_scroll_position_as_the_title() {
56 | let assets = Assets::init(None).unwrap();
57 | let page_state = PageState { scroll_top: 100.5, ..PageState::default() };
58 | let path = assets.build(&RenderedContent::default(), &page_state).unwrap();
59 |
60 | // Yes, it's included as the title, it's kind of dumb, but incredibly easy compared to the
61 | // alternative.
62 | assert_contains!(
63 | fs::read_to_string(&path).unwrap(),
64 | r#"{"scroll_top":100.5,"image_widths":{},"image_heights":{}}"#
65 | );
66 | }
67 |
68 | #[test]
69 | fn test_output_to_a_given_directory() {
70 | let tempdir = tempfile::tempdir().unwrap();
71 | let path = tempdir.path().to_path_buf();
72 | let mut assets = Assets::init(Some(path.clone())).unwrap();
73 |
74 | assert!(!path.join("index.html").exists());
75 | assets.build(&RenderedContent::default(), &PageState::default()).unwrap();
76 | assert!(path.join("index.html").exists());
77 |
78 | // Clearing asset tempdir should not remove explicitly given directory
79 | assets.clean_up();
80 | assert!(path.join("index.html").exists());
81 |
82 | // Dropping assets should also not remove directory
83 | drop(assets);
84 | assert!(path.join("index.html").exists());
85 | }
86 |
87 | #[test]
88 | fn test_will_create_output_dir_if_it_doesnt_exist() {
89 | let tempdir = tempfile::tempdir().unwrap();
90 | let path = tempdir.path().join("nested").join("output");
91 | let assets = Assets::init(Some(path.clone())).unwrap();
92 |
93 | assert!(!path.join("index.html").exists());
94 | assets.build(&RenderedContent::default(), &PageState::default()).unwrap();
95 | assert!(path.join("index.html").exists());
96 | }
97 |
98 | #[test]
99 | fn test_will_fail_if_output_dir_is_not_a_dir() {
100 | let tempfile = tempfile::NamedTempFile::new().unwrap();
101 | let path = tempfile.path().to_path_buf();
102 |
103 | assert_matches!(Assets::init(Some(path)), Err(_));
104 | }
105 |
--------------------------------------------------------------------------------
/src/background.rs:
--------------------------------------------------------------------------------
1 | //! Background monitoring for file changes.
2 | //!
3 | //! Whenever a file changes, we want to regenerate the HTML and send it to the UI for rendering to
4 | //! the user. This is done with the `init_update_loop` function.
5 |
6 | use std::sync::mpsc;
7 | use std::thread;
8 | use std::time::Duration;
9 | use std::marker::Send;
10 |
11 | use log::{debug, error, warn};
12 | use notify::{Watcher, RecursiveMode, DebouncedEvent, watcher};
13 |
14 | use crate::input::Config;
15 | use crate::markdown;
16 | use crate::ui;
17 |
18 | /// A common trait for `glib::Sender` and `std::mpsc::Sender`.
19 | ///
20 | /// Both of them have the exact same `send` method, down to the error type they use. Still, we need
21 | /// a shared trait to use in the `init_update_loop` function.
22 | ///
23 | /// In practice, we only use `glib::Sender` in "real code", but `std::mpsc::Sender` allows easier
24 | /// testing, so that's why this trait exists.
25 | ///
26 | pub trait Sender {
27 | /// Send a `ui::Event` to the receiver at the other end
28 | fn send(&mut self, event: ui::Event) -> Result<(), mpsc::SendError>;
29 | }
30 |
31 | impl Sender for glib::Sender {
32 | fn send(&mut self, event: ui::Event) -> Result<(), mpsc::SendError> {
33 | glib::Sender::::send(self, event)
34 | }
35 | }
36 |
37 | impl Sender for mpsc::Sender {
38 | fn send(&mut self, event: ui::Event) -> Result<(), mpsc::SendError> {
39 | mpsc::Sender::::send(self, event)
40 | }
41 | }
42 |
43 | /// The main background worker. Spawns a thread and uses the `notify` crate to listen for file changes.
44 | ///
45 | /// Input:
46 | ///
47 | /// - `renderer`: The struct that takes care of rendering the markdown file into HTML. Used to get
48 | /// the filename to monitor and to generate the HTML on update.
49 | /// - `ui_sender`: The channel to send `ui::Event` records to when a change is detected.
50 | ///
51 | /// A change to the main markdown file triggers a rerender and webview refresh. A change to the
52 | /// user-level configuration files is only going to trigger a refresh.
53 | ///
54 | pub fn init_update_loop(renderer: markdown::Renderer, mut ui_sender: S)
55 | where S: Sender + Send + 'static
56 | {
57 | thread::spawn(move || {
58 | let (watcher_sender, watcher_receiver) = mpsc::channel();
59 |
60 | let mut watcher = match watcher(watcher_sender, Duration::from_millis(200)) {
61 | Ok(w) => w,
62 | Err(e) => {
63 | warn!("Couldn't initialize watcher: {}", e);
64 | return;
65 | }
66 | };
67 |
68 | // Watch the parent directory so we can catch recreated files
69 | let main_watch_path = renderer.canonical_md_path.parent().
70 | unwrap_or(&renderer.canonical_md_path).
71 | to_owned();
72 |
73 | if let Err(e) = watcher.watch(&main_watch_path, RecursiveMode::NonRecursive) {
74 | warn!("Couldn't initialize watcher: {}", e);
75 | return;
76 | }
77 |
78 | let mut extra_watch_paths = vec![];
79 | let custom_css_path = Config::css_path();
80 |
81 | if watcher.watch(&custom_css_path, RecursiveMode::NonRecursive).is_ok() {
82 | debug!("Watching {}", custom_css_path.display());
83 | extra_watch_paths.push(custom_css_path);
84 | }
85 |
86 | loop {
87 | match watcher_receiver.recv() {
88 | Ok(DebouncedEvent::Write(file) | DebouncedEvent::Create(file)) => {
89 | debug!("File update/recreate detected: {}", file.display());
90 |
91 | if file == renderer.canonical_md_path {
92 | match renderer.run() {
93 | Ok(html) => {
94 | let _ = ui_sender.send(ui::Event::LoadHtml(html));
95 | },
96 | Err(e) => {
97 | error! {
98 | "Error rendering markdown ({}): {:?}",
99 | renderer.canonical_md_path.display(), e
100 | };
101 | }
102 | }
103 | } else if extra_watch_paths.contains(&file) {
104 | let _ = ui_sender.send(ui::Event::Reload);
105 | } else {
106 | debug!("Ignored file update event: {:?}", file)
107 | }
108 | },
109 | Ok(event) => debug!("Ignored watcher event: {:?}", event),
110 | Err(e) => error!("Error watching file for changes: {:?}", e),
111 | }
112 | }
113 | });
114 | }
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://crates.io/crates/quickmd)
4 | [](https://docs.rs/quickmd)
5 | [](https://crates.io/crates/quickmd)
6 |
7 | # QuickMD
8 |
9 | This project is a simple tool that solves a simple problem: I'd like to preview markdown files as they'll show up on Github, so I don't have to push my READMEs before I can tell whether they're formatted well enough. It ends up looking like this:
10 |
11 | 
12 |
13 | It's a Rust app that launches a GtkWebkit window that renders the compiled HTML of the given markdown file. It monitors this file for any changes and reloads. It uses a stylesheet that's literally copied off Github's markdown stylesheet.
14 |
15 | _Note: I have no idea if I'm allowed to use Github's stylesheet. The relevant file is in res/style/github.css, and if I am told I shouldn't be using it I'll go ahead and scrub it from git history._
16 |
17 | ## Installation
18 |
19 | ### From a release binary
20 |
21 | You should be able to find a compiled 64-bit linux binary in every release since v0.4.1. You can put it in your `$PATH` and launch it, as long as you have GTK3 and Webkit2Gtk installed on your system. On Arch Linux, you can install these like so:
22 |
23 | ``` .sh-session
24 | # pacman -S gtk3 webkit2gtk
25 | ```
26 |
27 | ### From source
28 |
29 | You'll need to have Rust and the `cargo` tool. The easiest way to get that done is through [rustup.rs](https://rustup.rs/).
30 |
31 | You'll also need the GTK3 and Webkit2Gtk _development_ files to be installed on your system. The Gtk-rs [installation page](https://www.gtk.org/docs/installations/) might be a good start.
32 |
33 | After that, you can build and install the app from `crates.io` using:
34 |
35 | ```
36 | cargo install quickmd
37 | ```
38 |
39 | Make sure that `~/.cargo/bin` is in your `PATH` so you can call the `quickmd` executable.
40 |
41 | ## Usage
42 |
43 | Running the app is as simple as:
44 |
45 | ```
46 | quickmd
47 | ```
48 |
49 | Pressing CTRL+Q will close the window. Running it with `--help` should provide more info on the available options. Here's how the output looks for me:
50 |
51 | ```
52 | quickmd 0.6.0
53 | A simple self-contained markdown previewer.
54 |
55 | Code highlighting via highlight.js version 9.18.1
56 |
57 | Edit configuration in: /home/andrew/.config/quickmd/config.yaml
58 | Add custom CSS in: /home/andrew/.config/quickmd/custom.css
59 |
60 | USAGE:
61 | quickmd [FLAGS] [OPTIONS] [input-file.md]
62 |
63 | FLAGS:
64 | -d, --debug
65 | Activates debug logging
66 |
67 | -h, --help
68 | Prints help information
69 |
70 | --install-default-config
71 | Creates a configuration file for later editing if one doesn't exist. Exits when done
72 |
73 | -V, --version
74 | Prints version information
75 |
76 | --no-watch
77 | Disables watching file for changes
78 |
79 |
80 | OPTIONS:
81 | --output
82 | Builds output HTML and other assets in the given directory instead of in a tempdir. Will be created if it
83 | doesn't exist. Not deleted on application exit
84 |
85 | ARGS:
86 |
87 | Markdown file to render. Use "-" to read markdown from STDIN (implies --no-watch). If not provided, the app
88 | will launch a file picker
89 | ```
90 |
91 | ## Features
92 |
93 | - Github-like rendering, though not guaranteed to be perfectly identical. Relying on whatever [pulldown-cmark](https://crates.io/crates/pulldown-cmark) provides, which is good enough for me.
94 |
95 | - Fast and seamless preview updates on file write.
96 |
97 | - Code highlighting via [highlight.js](https://highlightjs.org/). Currently, the relevant javascript is included via a CDN, which unfortunately means it won't work without an internet connection.
98 |
99 | - Ability to render STDIN, which allows partial rendering of target markdown. Try putting [this bit of Vimscript](https://github.com/AndrewRadev/Vimfiles/blob/f9e0c08dd280d13acb625d3370da399c39e14403/ftplugin/markdown.vim#L11-L15) in your `~/.vim/ftplugin/markdown.vim`, select a few lines and press `!`.
100 |
101 | - Scroll with Vi-like keybindings, Press `e` to spawn an editor, if configured.
102 |
103 | - Customizable keybindings. Check the API documentation or try `--install-default-config` to get an annotated config with examples.
104 |
105 | ## Configuration
106 |
107 | You can change the CSS of the preview HTML by writing a file named "custom.css" in the application's config directory. On a linux machine, it would be: `~/.config/quickmd/`.
108 |
109 | You can also change some configuration options in a config file. Run `quickmd` with `--install-default-config` to create that file with all the defaults and comments.
110 |
111 | Run `--help` to see where the config files will be located on your system, or press `F1` in the interface.
112 |
113 | The built-in CSS that is used is stored in [/res/style](./res/style) and the default config is in [/res/default_config.yaml](./res/default_config.yaml)
114 |
--------------------------------------------------------------------------------
/tests/test_ui_actions.rs:
--------------------------------------------------------------------------------
1 | use gdk::ModifierType;
2 | use gdk::keys::Key;
3 |
4 | use quickmd::input::MappingDefinition;
5 | use quickmd::ui::action::{Action, Keymaps};
6 |
7 | #[test]
8 | fn test_default_keybindings() {
9 | let keymaps = Keymaps::default();
10 |
11 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('j')), Action::SmallScrollDown);
12 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('k')), Action::SmallScrollUp);
13 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('J')), Action::BigScrollDown);
14 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('K')), Action::BigScrollUp);
15 |
16 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('e')), Action::LaunchEditor);
17 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('E')), Action::ExecEditor);
18 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('+')), Action::ZoomIn);
19 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('-')), Action::ZoomOut);
20 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('=')), Action::ZoomReset);
21 |
22 | assert_eq!(keymaps.get_action(ModifierType::CONTROL_MASK, Key::from_unicode('q')), Action::Quit);
23 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("F1")), Action::ShowHelp);
24 |
25 | }
26 |
27 | #[test]
28 | fn test_shift_and_capital_letter_equivalence_when_getting() {
29 | let keymaps = Keymaps::default();
30 |
31 | let big_j = keymaps.get_action(ModifierType::empty(), Key::from_name("J"));
32 | assert_eq!(big_j, Action::BigScrollDown);
33 |
34 | let shift_j = keymaps.get_action(ModifierType::SHIFT_MASK, Key::from_name("j"));
35 | assert_eq!(shift_j, Action::BigScrollDown);
36 | }
37 |
38 | #[test]
39 | fn test_setting_custom_mappings() {
40 | let mut keymaps = Keymaps::default();
41 |
42 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("q")), Action::Noop);
43 |
44 | keymaps.set_action(ModifierType::empty(), Key::from_name("q"), Action::Quit);
45 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("q")), Action::Quit);
46 |
47 | keymaps.set_action(ModifierType::empty(), Key::from_name("q"), Action::ZoomIn);
48 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("q")), Action::ZoomIn);
49 | }
50 |
51 | #[test]
52 | fn test_unsetting_a_default_mapping() {
53 | let mut keymaps = Keymaps::default();
54 |
55 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("j")), Action::SmallScrollDown);
56 |
57 | keymaps.set_action(ModifierType::empty(), Key::from_name("j"), Action::Noop);
58 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_name("j")), Action::Noop);
59 | }
60 |
61 | #[test]
62 | fn test_setting_non_alphabetic_shift_keys() {
63 | let mut keymaps = Keymaps::default();
64 |
65 | keymaps.set_action(ModifierType::empty(), Key::from_unicode('+'), Action::ScrollToTop);
66 | assert_eq!(keymaps.get_action(ModifierType::empty(), Key::from_unicode('+')), Action::ScrollToTop);
67 | assert_eq!(keymaps.get_action(ModifierType::SHIFT_MASK, Key::from_unicode('+')), Action::ScrollToTop);
68 |
69 | assert_ne!(keymaps.get_action(ModifierType::SHIFT_MASK, Key::from_unicode('=')), Action::ScrollToTop);
70 | }
71 |
72 | #[test]
73 | fn test_successfully_setting_mappings_from_the_config() {
74 | let mut keymaps = Keymaps::new();
75 |
76 | let mapping = MappingDefinition {
77 | key_char: Some('q'),
78 | key_name: None,
79 | mods: Vec::new(),
80 | action: Action::Quit,
81 | };
82 | assert!(keymaps.add_config_mappings(&[mapping]).is_ok());
83 |
84 | let mapping1 = MappingDefinition {
85 | key_char: None,
86 | key_name: Some(String::from("plus")),
87 | mods: vec![String::from("control")],
88 | action: Action::ScrollToBottom,
89 | };
90 | let mapping2 = MappingDefinition {
91 | key_char: None,
92 | key_name: Some(String::from("minus")),
93 | mods: vec![String::from("control"), String::from("shift"), String::from("alt")],
94 | action: Action::ScrollToTop,
95 | };
96 | assert!(keymaps.add_config_mappings(&[mapping1, mapping2]).is_ok());
97 |
98 | let action = keymaps.get_action(ModifierType::empty(), Key::from_unicode('q'));
99 | assert_eq!(action, Action::Quit);
100 |
101 | let action = keymaps.get_action(ModifierType::CONTROL_MASK, Key::from_unicode('+'));
102 | assert_eq!(action, Action::ScrollToBottom);
103 |
104 | let action = keymaps.get_action(
105 | ModifierType::CONTROL_MASK | ModifierType::SHIFT_MASK | ModifierType::MOD1_MASK,
106 | Key::from_unicode('-')
107 | );
108 | assert_eq!(action, Action::ScrollToTop);
109 | }
110 |
111 | #[test]
112 | fn test_invalid_mapping_from_config() {
113 | let mut keymaps = Keymaps::new();
114 |
115 | let mapping = MappingDefinition {
116 | key_char: None,
117 | key_name: None,
118 | mods: Vec::new(),
119 | action: Action::Quit,
120 | };
121 | assert!(keymaps.add_config_mappings(&[mapping]).is_err());
122 |
123 | let mapping = MappingDefinition {
124 | key_char: Some('q'),
125 | key_name: Some(String::from("q")),
126 | mods: Vec::new(),
127 | action: Action::Quit,
128 | };
129 | assert!(keymaps.add_config_mappings(&[mapping]).is_err());
130 | }
131 |
--------------------------------------------------------------------------------
/src/ui/action.rs:
--------------------------------------------------------------------------------
1 | //! Actions on the UI triggered by keybindings.
2 |
3 | use std::collections::HashMap;
4 |
5 | use anyhow::anyhow;
6 | use gdk::ModifierType;
7 | use gdk::keys::{self, Key};
8 | use log::debug;
9 | use serde::{Serialize, Deserialize};
10 |
11 | use crate::input::MappingDefinition;
12 |
13 | /// Mappable actions
14 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
15 | pub enum Action {
16 | /// Placeholder action to allow unmapping keys
17 | Noop,
18 |
19 | /// Scroll up by a small step. Default: `k`
20 | SmallScrollUp,
21 | /// Scroll down by a small step. Default: `j`
22 | SmallScrollDown,
23 |
24 | /// Scroll up by a large step. Default: `K`
25 | BigScrollUp,
26 | /// Scroll down by a large step. Default: `J`
27 | BigScrollDown,
28 |
29 | /// Scroll to the top of the document. Default: `g`
30 | ScrollToTop,
31 | /// Scroll to the bottom of the document. Default: `G`
32 | ScrollToBottom,
33 |
34 | /// Quit the entire application. Default: `ctrl+q`
35 | Quit,
36 |
37 | /// Launch an editor instance if it's configured. Default: `e`
38 | LaunchEditor,
39 | /// Exec the current process into an editor instance if it's configured (and it's possible on
40 | /// the OS). Default: `E`
41 | ExecEditor,
42 |
43 | /// Zoom the browser in by 10%. Default: `+`
44 | ZoomIn,
45 | /// Zoom the browser out by 10%. Default: `-`
46 | ZoomOut,
47 | /// Reset the zoom level to the configured starting point. Default: `=`
48 | ZoomReset,
49 |
50 | /// Show a help popup. Default: `F1`
51 | ShowHelp,
52 | }
53 |
54 | impl Default for Action {
55 | fn default() -> Self {
56 | Action::Noop
57 | }
58 | }
59 |
60 | /// A mapping from key bindings to all the different UI actions. Initialized with a full set of
61 | /// defaults, which can be overridden by configuration.
62 | ///
63 | #[derive(Clone)]
64 | pub struct Keymaps {
65 | mappings: HashMap<(ModifierType, Key), Action>,
66 | }
67 |
68 | impl Default for Keymaps {
69 | fn default() -> Self {
70 | let mut keymaps = Self::new();
71 |
72 | // Scroll with j/k, J/K:
73 | keymaps.set_action(ModifierType::empty(), keys::constants::j, Action::SmallScrollDown);
74 | keymaps.set_action(ModifierType::empty(), keys::constants::k, Action::SmallScrollUp);
75 | keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::j, Action::BigScrollDown);
76 | keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::k, Action::BigScrollUp);
77 | // Jump to the top/bottom with g/G
78 | keymaps.set_action(ModifierType::empty(), keys::constants::g, Action::ScrollToTop);
79 | keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::g, Action::ScrollToBottom);
80 | // Ctrl+Q to quit
81 | keymaps.set_action(ModifierType::CONTROL_MASK, keys::constants::q, Action::Quit);
82 | // e, E for editor integration
83 | keymaps.set_action(ModifierType::empty(), keys::constants::e, Action::LaunchEditor);
84 | keymaps.set_action(ModifierType::SHIFT_MASK, keys::constants::e, Action::ExecEditor);
85 | // +/-/= for zoom control
86 | keymaps.set_action(ModifierType::empty(), keys::constants::plus, Action::ZoomIn);
87 | keymaps.set_action(ModifierType::empty(), keys::constants::minus, Action::ZoomOut);
88 | keymaps.set_action(ModifierType::empty(), keys::constants::equal, Action::ZoomReset);
89 | // F1 to show help popup
90 | keymaps.set_action(ModifierType::empty(), keys::constants::F1, Action::ShowHelp);
91 |
92 | keymaps
93 | }
94 | }
95 |
96 | impl Keymaps {
97 | /// Create an empty set of keymaps.
98 | ///
99 | pub fn new() -> Self {
100 | Self { mappings: HashMap::new() }
101 | }
102 |
103 | /// Parse the given mappings as described in [`crate::input::Config`]
104 | ///
105 | pub fn add_config_mappings(&mut self, mappings: &[MappingDefinition]) -> anyhow::Result<()> {
106 | for mapping in mappings {
107 | let mut modifiers = ModifierType::empty();
108 | for m in &mapping.mods {
109 | match m.as_str() {
110 | "control" => { modifiers |= ModifierType::CONTROL_MASK; }
111 | "shift" => { modifiers |= ModifierType::SHIFT_MASK; }
112 | "alt" => { modifiers |= ModifierType::MOD1_MASK; }
113 | _ => {
114 | { return Err(anyhow!("Unknown modifier: {}", m)); }
115 | },
116 | }
117 | }
118 |
119 | if mapping.key_char.is_some() && mapping.key_name.is_some() {
120 | return Err(
121 | anyhow!("Both `key_char` or `key_name` given, please pick just one: {:?}", mapping)
122 | );
123 | }
124 |
125 | let key =
126 | if let Some(c) = mapping.key_char {
127 | Key::from_unicode(c)
128 | } else if let Some(name) = &mapping.key_name {
129 | Key::from_name(name)
130 | } else {
131 | return Err(anyhow!("No `key_char` or `key_name` given: {:?}", mapping));
132 | };
133 |
134 | self.set_action(modifiers, key, mapping.action.clone());
135 | debug!("Defined custom mapping: {:?}", mapping);
136 | }
137 |
138 | Ok(())
139 | }
140 |
141 | /// Get the action corresponding to the given modifiers and key. Uppercase unicode letters like
142 | /// are normalized to a lowercase letter + shift.
143 | ///
144 | pub fn get_action(&self, modifiers: ModifierType, key: Key) -> Action {
145 | let (key, modifiers) = Self::normalize_input(key, modifiers);
146 | self.mappings.get(&(modifiers, key)).cloned().unwrap_or(Action::Noop)
147 | }
148 |
149 | /// Set the action corresponding to the given modifiers and key. Could override existing
150 | /// actions. Setting `Action::Noop` is the way to "unmap" a keybinding. Uppercase unicode
151 | /// letters like are normalized to a lowercase letter + shift.
152 | ///
153 | pub fn set_action(&mut self, modifiers: ModifierType, key: Key, action: Action) {
154 | let (key, modifiers) = Self::normalize_input(key, modifiers);
155 | self.mappings.insert((modifiers, key), action);
156 | }
157 |
158 | fn normalize_input(mut key: Key, mut modifiers: ModifierType) -> (Key, ModifierType) {
159 | // If we get something considered an "upper" key, that means shift is being held. This is
160 | // not just for A -> S-a, but also for + -> = (though the + is not transformed).
161 | if key.is_upper() {
162 | key = key.to_lower();
163 | modifiers.insert(ModifierType::SHIFT_MASK);
164 | }
165 |
166 | (key, modifiers)
167 | }
168 | }
169 |
--------------------------------------------------------------------------------
/res/style/github.css:
--------------------------------------------------------------------------------
1 | @charset "UTF-8";
2 |
3 | main {
4 | font-size: 14px;
5 | line-height: 1.6;
6 | overflow: hidden
7 | }
8 | main>*: first-child {
9 | margin-top: 0 !important
10 | }
11 | main>*: last-child {
12 | margin-bottom: 0 !important
13 | }
14 | main a.absent {
15 | color: #c00
16 | }
17 | main a.anchor {
18 | display: block;
19 | padding-left: 30px;
20 | margin-left: -30px;
21 | cursor: pointer;
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | bottom: 0
26 | }
27 | main h1,main h2,main h3,main h4,main h5,main h6 {
28 | margin: 20px 0 10px;
29 | padding: 0;
30 | font-weight: bold;
31 | -webkit-font-smoothing: antialiased;
32 | cursor: text;
33 | position: relative
34 | }
35 | main h1 .mini-icon-link,main h2 .mini-icon-link,main h3 .mini-icon-link,main h4 .mini-icon-link,main h5 .mini-icon-link,main h6 .mini-icon-link {
36 | display: none;
37 | color: #000
38 | }
39 | main h1: hover a.anchor,main h2: hover a.anchor,main h3: hover a.anchor,main h4: hover a.anchor,main h5: hover a.anchor,main h6: hover a.anchor {
40 | text-decoration: none;
41 | line-height: 1;
42 | padding-left: 0;
43 | margin-left: -22px;
44 | top: 15%
45 | }
46 | main h1: hover a.anchor .mini-icon-link,main h2: hover a.anchor .mini-icon-link,main h3: hover a.anchor .mini-icon-link,main h4: hover a.anchor .mini-icon-link,main h5: hover a.anchor .mini-icon-link,main h6: hover a.anchor .mini-icon-link {
47 | display: inline-block
48 | }
49 | main h1 tt,main h1 code,main h2 tt,main h2 code,main h3 tt,main h3 code,main h4 tt,main h4 code,main h5 tt,main h5 code,main h6 tt,main h6 code {
50 | font-size: inherit
51 | }
52 | main h1 {
53 | font-size: 28px;
54 | color: #000
55 | }
56 | main h2 {
57 | font-size: 24px;
58 | border-bottom: 1px solid #ccc;
59 | color: #000
60 | }
61 | main h3 {
62 | font-size: 18px
63 | }
64 | main h4 {
65 | font-size: 16px
66 | }
67 | main h5 {
68 | font-size: 14px
69 | }
70 | main h6 {
71 | color: #777;
72 | font-size: 14px
73 | }
74 | main p,main blockquote,main ul,main ol,main dl,main table,main pre {
75 | margin: 15px 0
76 | }
77 | main hr {
78 | background: transparent url("https: //a248.e.akamai.net/assets.github.com/assets/primer/markdown/dirty-shade-6ead57f83b0f117a80ba77232aff0673bfd71263.png") repeat-x 0 0;
79 | border: 0 none;
80 | color: #ccc;
81 | height: 4px;
82 | padding: 0
83 | }
84 | main>h2: first-child,main>h1: first-child,main>h1: first-child+h2,main>h3: first-child,main>h4: first-child,main>h5: first-child,main>h6: first-child {
85 | margin-top: 0;
86 | padding-top: 0
87 | }
88 | main a: first-child h1,main a: first-child h2,main a: first-child h3,main a: first-child h4,main a: first-child h5,main a: first-child h6 {
89 | margin-top: 0;
90 | padding-top: 0
91 | }
92 | main h1+p,main h2+p,main h3+p,main h4+p,main h5+p,main h6+p {
93 | margin-top: 0
94 | }
95 | main li p.first {
96 | display: inline-block
97 | }
98 | main ul,main ol {
99 | padding-left: 30px
100 | }
101 | main ul.no-list,main ol.no-list {
102 | list-style-type: none;
103 | padding: 0
104 | }
105 | main ul li>: first-child,main ul li ul: first-of-type,main ol li>: first-child,main ol li ul: first-of-type {
106 | margin-top: 0px
107 | }
108 | main ul ul,main ul ol,main ol ol,main ol ul {
109 | margin-bottom: 0
110 | }
111 | main dl {
112 | padding: 0
113 | }
114 | main dl dt {
115 | font-size: 14px;
116 | font-weight: bold;
117 | font-style: italic;
118 | padding: 0;
119 | margin: 15px 0 5px
120 | }
121 | main dl dt: first-child {
122 | padding: 0
123 | }
124 | main dl dt>: first-child {
125 | margin-top: 0px
126 | }
127 | main dl dt>: last-child {
128 | margin-bottom: 0px
129 | }
130 | main dl dd {
131 | margin: 0 0 15px;
132 | padding: 0 15px
133 | }
134 | main dl dd>: first-child {
135 | margin-top: 0px
136 | }
137 | main dl dd>: last-child {
138 | margin-bottom: 0px
139 | }
140 | main blockquote {
141 | border-left: 4px solid #DDD;
142 | padding: 0 15px;
143 | color: #777
144 | }
145 | main blockquote>: first-child {
146 | margin-top: 0px
147 | }
148 | main blockquote>: last-child {
149 | margin-bottom: 0px
150 | }
151 | main table th {
152 | font-weight: bold
153 | }
154 | main table th,main table td {
155 | border: 1px solid #ccc;
156 | padding: 6px 13px
157 | }
158 | main table tr {
159 | border-top: 1px solid #ccc;
160 | background-color: #fff
161 | }
162 | main table tr: nth-child(2n) {
163 | background-color: #f8f8f8
164 | }
165 | main img {
166 | max-width: 100%;
167 | -moz-box-sizing: border-box;
168 | box-sizing: border-box
169 | }
170 | main span.frame {
171 | display: block;
172 | overflow: hidden
173 | }
174 | main span.frame>span {
175 | border: 1px solid #ddd;
176 | display: block;
177 | float: left;
178 | overflow: hidden;
179 | margin: 13px 0 0;
180 | padding: 7px;
181 | width: auto
182 | }
183 | main span.frame span img {
184 | display: block;
185 | float: left
186 | }
187 | main span.frame span span {
188 | clear: both;
189 | color: #333;
190 | display: block;
191 | padding: 5px 0 0
192 | }
193 | main span.align-center {
194 | display: block;
195 | overflow: hidden;
196 | clear: both
197 | }
198 | main span.align-center>span {
199 | display: block;
200 | overflow: hidden;
201 | margin: 13px auto 0;
202 | text-align: center
203 | }
204 | main span.align-center span img {
205 | margin: 0 auto;
206 | text-align: center
207 | }
208 | main span.align-right {
209 | display: block;
210 | overflow: hidden;
211 | clear: both
212 | }
213 | main span.align-right>span {
214 | display: block;
215 | overflow: hidden;
216 | margin: 13px 0 0;
217 | text-align: right
218 | }
219 | main span.align-right span img {
220 | margin: 0;
221 | text-align: right
222 | }
223 | main span.float-left {
224 | display: block;
225 | margin-right: 13px;
226 | overflow: hidden;
227 | float: left
228 | }
229 | main span.float-left span {
230 | margin: 13px 0 0
231 | }
232 | main span.float-right {
233 | display: block;
234 | margin-left: 13px;
235 | overflow: hidden;
236 | float: right
237 | }
238 | main span.float-right>span {
239 | display: block;
240 | overflow: hidden;
241 | margin: 13px auto 0;
242 | text-align: right
243 | }
244 | main code,main tt {
245 | margin: 0 2px;
246 | padding: 0px 5px;
247 | border: 1px solid #eaeaea;
248 | background-color: #f8f8f8;
249 | border-radius: 3px
250 | }
251 | main code {
252 | white-space: nowrap
253 | }
254 | main pre>code {
255 | margin: 0;
256 | padding: 0;
257 | white-space: pre;
258 | border: none;
259 | background: transparent
260 | }
261 | main .highlight pre,main pre {
262 | background-color: #f8f8f8;
263 | border: 1px solid #ccc;
264 | font-size: 13px;
265 | line-height: 19px;
266 | overflow: auto;
267 | padding: 6px 10px;
268 | border-radius: 3px
269 | }
270 | main pre code,main pre tt {
271 | margin: 0;
272 | padding: 0;
273 | background-color: transparent;
274 | border: none
275 | }
276 |
277 | main a {
278 | color: #4183c4;
279 | text-decoration: none;
280 | }
281 |
282 | main a:hover {
283 | text-decoration: underline;
284 | }
285 |
286 | main pre,
287 | main code {
288 | font-size: 12px;
289 | font-family: Consolas, "Liberation Mono", Courier, monospace;
290 | }
291 |
--------------------------------------------------------------------------------
/src/assets.rs:
--------------------------------------------------------------------------------
1 | //! Management of external assets like Javascript and CSS.
2 | //!
3 | //! The files are stored into the binary as strings and written to the filesystem when the
4 | //! application runs. For the HTML file, this allows the webview to load a URL instead of a string
5 | //! body, which makes reloading smoother (update the file, refresh).
6 | //!
7 | //! For the other assets, it means the HTML can refer to local files instead of embedding the
8 | //! contents as `"#, root_url).
133 | unwrap();
134 |
135 | for language in &content.code_languages {
136 | writeln!(
137 | hl_tags,
138 | r#""#,
139 | root_url, language
140 | ).unwrap();
141 | }
142 |
143 | writeln!(hl_tags, r#""#).unwrap();
144 | }
145 |
146 | debug!("Building HTML:");
147 | debug!(" > custom_css_path = {:?}", custom_css_path);
148 | debug!(" > page_state = {:?}", json_state);
149 | debug!(" > code languages = {:?}", content.code_languages);
150 |
151 | let page = format! {
152 | include_str!("../res/layout.html"),
153 | custom_css_path = custom_css_path.display(),
154 | body = content.html,
155 | hl_tags = hl_tags,
156 | page_state = json_state,
157 | };
158 |
159 | let html_path = output_path.join("index.html");
160 | fs::write(&html_path, page.as_bytes())?;
161 |
162 | Ok(html_path)
163 | }
164 |
165 | /// The path on the filesystem where the HTML and other assets go. Could be a temporary
166 | /// directory, or the one given at construction time.
167 | ///
168 | pub fn output_path(&self) -> anyhow::Result {
169 | match (&self.real_dir, &self.temp_dir) {
170 | (Some(path_buf), _) => Ok(path_buf.clone()),
171 | (_, Some(temp_dir)) => Ok(temp_dir.path().to_path_buf()),
172 | _ => Err(anyhow!("Assets don't have an output dir, there might be a synchronization error"))
173 | }
174 | }
175 |
176 | /// Delete the temporary directory used for building assets, if there is one. This should
177 | /// happen automatically on drop, but a GTK-level exit doesn't seem to unroll the stack, so we
178 | /// may need to delete things explicitly.
179 | ///
180 | /// If deletion fails, we quietly print a warning. Multiple (successful or failed) deletions
181 | /// are a noop.
182 | ///
183 | pub fn clean_up(&mut self) {
184 | if let Some(temp_dir) = self.temp_dir.take() {
185 | let path = temp_dir.path();
186 | fs::remove_dir_all(path).unwrap_or_else(|_| {
187 | warn!("Couldn't delete temporary dir: {}", path.display());
188 | });
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/src/ui/mod.rs:
--------------------------------------------------------------------------------
1 | //! The GTK user interface.
2 |
3 | pub mod action;
4 | pub mod browser;
5 | pub mod dialogs;
6 |
7 | use std::path::{PathBuf, Path};
8 | use std::process::Command;
9 |
10 | use gtk::prelude::*;
11 | use log::{debug, warn, error};
12 | use pathbuftools::PathBufTools;
13 |
14 | use crate::assets::Assets;
15 | use crate::input::{InputFile, Config};
16 | use crate::markdown::RenderedContent;
17 | use crate::ui::action::{Action, Keymaps};
18 | use crate::ui::browser::Browser;
19 | use crate::ui::dialogs::open_help_dialog;
20 |
21 | /// The container for all the GTK widgets of the app -- window, webview, etc.
22 | /// All of these are reference-counted, so should be cheap to clone.
23 | ///
24 | #[derive(Clone)]
25 | pub struct App {
26 | window: gtk::Window,
27 | browser: Browser,
28 | assets: Assets,
29 | filename: PathBuf,
30 | config: Config,
31 | }
32 |
33 | impl App {
34 | /// Construct a new app. Input params:
35 | ///
36 | /// - input_file: Used as the window title and for other actions on the file.
37 | /// - assets: Encapsulates the HTML layout that will be wrapping the rendered markdown.
38 | ///
39 | /// Initialization could fail due to a `WebContext` failure.
40 | ///
41 | pub fn init(config: Config, input_file: InputFile, assets: Assets) -> anyhow::Result {
42 | let window = gtk::Window::new(gtk::WindowType::Toplevel);
43 | window.set_default_size(1024, 768);
44 |
45 | if let Ok(asset_path) = assets.output_path() {
46 | if let Ok(icon) = gdk_pixbuf::Pixbuf::from_file(asset_path.join("icon.png")) {
47 | window.set_icon(Some(&icon));
48 | }
49 | }
50 |
51 | let title = match &input_file {
52 | InputFile::Filesystem(p) => format!("{} - Quickmd", p.short_path().display()),
53 | InputFile::Stdin(_) => String::from("Quickmd"),
54 | };
55 | window.set_title(&title);
56 |
57 | let browser = Browser::new(config.clone())?;
58 | browser.attach_to(&window);
59 |
60 | Ok(App { window, browser, assets, config, filename: input_file.path().to_path_buf() })
61 | }
62 |
63 | /// Start listening to events from the `ui_receiver` and trigger the relevant methods on the
64 | /// `App`. Doesn't block.
65 | ///
66 | pub fn init_render_loop(&self, ui_receiver: glib::Receiver) {
67 | let mut app_clone = self.clone();
68 |
69 | ui_receiver.attach(None, move |event| {
70 | match event {
71 | Event::LoadHtml(content) => {
72 | app_clone.load_content(&content).
73 | unwrap_or_else(|e| warn!("Couldn't update HTML: {}", e))
74 | },
75 | Event::Reload => app_clone.reload(),
76 | }
77 | glib::Continue(true)
78 | });
79 | }
80 |
81 | /// Actually start the UI, blocking the main thread.
82 | ///
83 | pub fn run(&mut self) {
84 | self.connect_events();
85 | self.window.show_all();
86 |
87 | gtk::main();
88 |
89 | self.assets.clean_up();
90 | }
91 |
92 | fn load_content(&mut self, content: &RenderedContent) -> anyhow::Result<()> {
93 | let page_state = self.browser.get_page_state();
94 | let output_path = self.assets.build(content, &page_state)?;
95 |
96 | debug!("Loading HTML:");
97 | debug!(" > output_path = {}", output_path.display());
98 |
99 | self.browser.load_uri(&format!("file://{}", output_path.display()));
100 | Ok(())
101 | }
102 |
103 | fn reload(&self) {
104 | self.browser.reload();
105 | }
106 |
107 | fn connect_events(&self) {
108 | let filename = self.filename.clone();
109 | let editor_command = self.config.editor_command.clone();
110 |
111 | let mut keymaps = Keymaps::default();
112 | keymaps.add_config_mappings(&self.config.mappings).unwrap_or_else(|e| {
113 | error!("Mapping parsing error: {}", e);
114 | });
115 |
116 | // Key presses mapped to repeatable events:
117 | let browser = self.browser.clone();
118 | let keymaps_clone = keymaps.clone();
119 | self.window.connect_key_press_event(move |_window, event| {
120 | let keyval = event.keyval();
121 | let keystate = event.state();
122 |
123 | match keymaps_clone.get_action(keystate, keyval) {
124 | Action::SmallScrollDown => browser.execute_js("window.scrollBy(0, 70)"),
125 | Action::BigScrollDown => browser.execute_js("window.scrollBy(0, 250)"),
126 | Action::SmallScrollUp => browser.execute_js("window.scrollBy(0, -70)"),
127 | Action::BigScrollUp => browser.execute_js("window.scrollBy(0, -250)"),
128 | Action::ScrollToTop => browser.execute_js("window.scroll({top: 0})"),
129 | Action::ScrollToBottom => {
130 | browser.execute_js("window.scroll({top: document.body.scrollHeight})")
131 | },
132 | _ => (),
133 | }
134 | Inhibit(false)
135 | });
136 |
137 | // Key releases mapped to one-time events:
138 | let browser = self.browser.clone();
139 | let keymaps_clone = keymaps.clone();
140 | self.window.connect_key_release_event(move |window, event| {
141 | let keyval = event.keyval();
142 | let keystate = event.state();
143 |
144 | match keymaps_clone.get_action(keystate, keyval) {
145 | Action::LaunchEditor => {
146 | debug!("Launching an editor");
147 | launch_editor(&editor_command, &filename);
148 | },
149 | Action::ExecEditor => {
150 | debug!("Exec-ing into an editor");
151 | exec_editor(&editor_command, &filename);
152 | },
153 | Action::ZoomIn => browser.zoom_in(),
154 | Action::ZoomOut => browser.zoom_out(),
155 | Action::ZoomReset => browser.zoom_reset(),
156 | Action::ShowHelp => { open_help_dialog(window); },
157 | Action::Quit => gtk::main_quit(),
158 | _ => (),
159 | }
160 | Inhibit(false)
161 | });
162 |
163 | // On Ctrl+Scroll, zoom:
164 | let browser = self.browser.clone();
165 | self.window.connect_scroll_event(move |_window, event| {
166 | if event.state().contains(gdk::ModifierType::CONTROL_MASK) {
167 | match event.direction() {
168 | gdk::ScrollDirection::Up => browser.zoom_in(),
169 | gdk::ScrollDirection::Down => browser.zoom_out(),
170 | _ => (),
171 | }
172 | }
173 |
174 | Inhibit(false)
175 | });
176 |
177 | self.window.connect_delete_event(|_, _| {
178 | gtk::main_quit();
179 | Inhibit(false)
180 | });
181 | }
182 | }
183 |
184 | /// Events that trigger UI changes.
185 | ///
186 | #[derive(Debug)]
187 | pub enum Event {
188 | /// Load the given content into the webview.
189 | LoadHtml(RenderedContent),
190 |
191 | /// Refresh the webview.
192 | Reload,
193 | }
194 |
195 | #[cfg(target_family="unix")]
196 | fn exec_editor(editor_command: &[String], file_path: &Path) {
197 | if let Some(mut editor) = build_editor_command(editor_command, file_path) {
198 | gtk::main_quit();
199 |
200 | use std::os::unix::process::CommandExt;
201 |
202 | let _ = editor.exec();
203 | }
204 | }
205 |
206 | #[cfg(not(target_family="unix"))]
207 | fn exec_editor(_editor_command: &[String], _filename_string: &Path) {
208 | warn!("Not on a UNIX system, can't exec to a text editor");
209 | }
210 |
211 | fn launch_editor(editor_command: &[String], file_path: &Path) {
212 | if let Some(mut editor) = build_editor_command(editor_command, file_path) {
213 | if let Err(e) = editor.spawn() {
214 | warn!("Couldn't launch editor ({:?}): {}", editor_command, e);
215 | }
216 | }
217 | }
218 |
219 | fn build_editor_command(editor_command: &[String], file_path: &Path) -> Option {
220 | let executable = editor_command.get(0).or_else(|| {
221 | warn!("No \"editor\" defined in the config ({})", Config::yaml_path().display());
222 | None
223 | })?;
224 |
225 | let mut command = Command::new(executable);
226 |
227 | for arg in editor_command.iter().skip(1) {
228 | if arg == "{path}" {
229 | command.arg(file_path);
230 | } else {
231 | command.arg(arg);
232 | }
233 | }
234 |
235 | Some(command)
236 | }
237 |
--------------------------------------------------------------------------------
/src/input.rs:
--------------------------------------------------------------------------------
1 | //! Input and config handling.
2 | //!
3 | //! This includes command-line options and settings from the YAML config. Potentially the place to
4 | //! handle any other type of configuration and input to the application.
5 |
6 | use std::fs::File;
7 | use std::io;
8 | use std::path::{PathBuf, Path};
9 | use std::rc::Rc;
10 |
11 | use anyhow::anyhow;
12 | use directories::ProjectDirs;
13 | use log::{debug, error};
14 | use serde::{Serialize, Deserialize};
15 | use structopt::StructOpt;
16 | use tempfile::NamedTempFile;
17 |
18 | use crate::assets::HIGHLIGHT_JS_VERSION;
19 | use crate::ui::action::Action;
20 |
21 | /// Command-line options. Managed by [`structopt`].
22 | #[derive(Debug, StructOpt)]
23 | pub struct Options {
24 | /// Activates debug logging
25 | #[structopt(short, long)]
26 | pub debug: bool,
27 |
28 | /// Markdown file to render. Use "-" to read markdown from STDIN (implies --no-watch). If not
29 | /// provided, the app will launch a file picker
30 | #[structopt(name = "input-file.md", parse(from_os_str))]
31 | pub input_file: Option,
32 |
33 | /// Disables watching file for changes
34 | #[structopt(long = "no-watch", parse(from_flag = std::ops::Not::not))]
35 | pub watch: bool,
36 |
37 | /// Builds output HTML and other assets in the given directory instead of in a tempdir.
38 | /// Will be created if it doesn't exist. Not deleted on application exit.
39 | #[structopt(long = "output", name = "directory")]
40 | pub output_dir: Option,
41 |
42 | /// Creates a configuration file for later editing if one doesn't exist. Exits when done.
43 | #[structopt(long)]
44 | pub install_default_config: bool,
45 | }
46 |
47 | impl Options {
48 | /// Creates a new instance by parsing input args. Apart from just running [`structopt`]'s
49 | /// initialization, it also adds some additional information to the description that depends on
50 | /// the current environment.
51 | ///
52 | pub fn build() -> Self {
53 | let description = &[
54 | "A simple self-contained markdown previewer. ",
55 | "",
56 | &format!("Code highlighting via highlight.js version {}", HIGHLIGHT_JS_VERSION),
57 | "",
58 | &format!("Edit configuration in: {}", Config::yaml_path().display()),
59 | &format!("Add custom CSS in: {}", Config::css_path().display()),
60 | ].join("\n");
61 |
62 | let options_app = Options::clap().
63 | long_about(description.as_str());
64 |
65 | Options::from_clap(&options_app.get_matches())
66 | }
67 |
68 | /// Start logging based on input flags.
69 | ///
70 | /// With `--debug`:
71 | /// - All logs
72 | /// - Timestamps
73 | /// - Module path context
74 | ///
75 | /// Otherwise:
76 | /// - Only warnings and errors
77 | /// - Minimal formatting
78 | ///
79 | pub fn init_logging(&self) {
80 | if self.debug {
81 | env_logger::builder().
82 | filter_level(log::LevelFilter::Debug).
83 | init();
84 | } else {
85 | env_logger::builder().
86 | format_module_path(false).
87 | format_timestamp(None).
88 | filter_level(log::LevelFilter::Warn).
89 | init();
90 | }
91 | }
92 | }
93 |
94 | /// Configuration that controls the behaviour of the app. Saved in a file in the standard app
95 | /// config directory named "config.yaml".
96 | ///
97 | #[derive(Debug, Clone, Serialize, Deserialize)]
98 | #[serde(default)]
99 | pub struct Config {
100 | /// The zoom level of the page. Defaults to 1.0, but on a HiDPI screen should be set to a
101 | /// higher value.
102 | ///
103 | pub zoom: f64,
104 |
105 | /// The external editor to launch when editing is requested. It defaults to an empty vector,
106 | /// which will produce a command-line warning when it's attempted.
107 | ///
108 | pub editor_command: Vec,
109 |
110 | /// Custom mappings. See documentation of [`MappingDefinition`] for details.
111 | pub mappings: Vec,
112 | }
113 |
114 | /// A single description of a mapping from a keybinding to a UI action. The fields `key_char` and
115 | /// `key_name` are exclusive, which is validated in [`crate::ui::action::Keymaps`].
116 | ///
117 | #[derive(Clone, Debug, Default, Serialize, Deserialize)]
118 | #[serde(default)]
119 | pub struct MappingDefinition {
120 | /// A descriptor, passed along to [`gdk::keys::Key::from_unicode`]
121 | pub key_char: Option,
122 |
123 | /// A descriptor, passed along to [`gdk::keys::Key::from_name`]
124 | pub key_name: Option,
125 |
126 | /// A list of key modifiers, either "control", "shift", or "alt"
127 | pub mods: Vec,
128 |
129 | /// The action mapped to this key combination
130 | pub action: Action,
131 | }
132 |
133 | impl Default for Config {
134 | fn default() -> Self {
135 | Self { zoom: 1.0, editor_command: Vec::new(), mappings: Vec::new() }
136 | }
137 | }
138 |
139 | impl Config {
140 | /// Loads the config from its standard location, or returns None if the file couldn't be found
141 | /// or is invalid.
142 | ///
143 | pub fn load() -> Option {
144 | let yaml_path = Self::yaml_path();
145 | let config_file = File::open(&yaml_path).or_else(|_| {
146 | // If "config.yaml" is missing, check for "config.yml" just in case
147 | let yml_path = yaml_path.with_extension("yml");
148 | File::open(&yml_path).map_err(|_| {
149 | debug!("Didn't find config file: {} (or {})", yaml_path.display(), yml_path.display());
150 | })
151 | }).ok()?;
152 |
153 | serde_yaml::from_reader(&config_file).map_err(|e| {
154 | error!("Couldn't parse YAML config file: {}", e);
155 | }).ok()
156 | }
157 |
158 | /// Gets the path to the default YAML config in the standard config location.
159 | pub fn yaml_path() -> PathBuf {
160 | ProjectDirs::from("com", "andrewradev", "quickmd").
161 | map(|pd| pd.config_dir().join("config.yaml")).
162 | unwrap_or_else(|| PathBuf::from("./quickmd.yaml"))
163 | }
164 |
165 | /// Gets the path to the custom CSS config in the standard config location.
166 | pub fn css_path() -> PathBuf {
167 | ProjectDirs::from("com", "andrewradev", "quickmd").
168 | map(|pd| pd.config_dir().join("custom.css")).
169 | unwrap_or_else(|| PathBuf::from("./quickmd.css"))
170 | }
171 |
172 | /// Attempts to install a config file with defaults. Returns an error if a file already exists
173 | /// in the expected location.
174 | ///
175 | pub fn try_install_default() -> anyhow::Result<()> {
176 | let yaml_path = Config::yaml_path();
177 |
178 | if yaml_path.exists() {
179 | Err(anyhow!("An existing file was found at: {}\n\
180 | If you want to replace it, please delete it first", yaml_path.display()))
181 | } else {
182 | yaml_path.parent().map(std::fs::create_dir_all);
183 | Ok(std::fs::write(yaml_path, include_str!("../res/default_config.yaml"))?)
184 | }
185 | }
186 | }
187 |
188 | /// The file used as input to the application. Could be an existing file path or STDIN.
189 | #[derive(Debug, Clone)]
190 | pub enum InputFile {
191 | /// A path representing a file on the filesystem.
192 | Filesystem(PathBuf),
193 |
194 | /// STDIN, written to a named tempfile. It's packaged in an Rc, so we can safely clone the
195 | /// structure.
196 | Stdin(Rc),
197 | }
198 |
199 | impl InputFile {
200 | /// Construct an `InputFile` based on the given path.
201 | ///
202 | /// If the path is "-", the given `contents` are assumed to be STDIN, they're written down in a
203 | /// tempfile and returned. Otherwise, that parameter is ignored.
204 | ///
205 | pub fn from(path: &Path, mut contents: impl io::Read) -> anyhow::Result {
206 | if path == PathBuf::from("-") {
207 | let mut tempfile = NamedTempFile::new()?;
208 | io::copy(&mut contents, &mut tempfile)?;
209 |
210 | Ok(InputFile::Stdin(Rc::new(tempfile)))
211 | } else {
212 | Ok(InputFile::Filesystem(path.to_path_buf()))
213 | }
214 | }
215 |
216 | /// Get the path to a real file on the filesystem.
217 | pub fn path(&self) -> &Path {
218 | match self {
219 | Self::Filesystem(path_buf) => path_buf.as_path(),
220 | Self::Stdin(tempfile) => tempfile.path(),
221 | }
222 | }
223 |
224 | /// Only true if the struct represents an actual file.
225 | pub fn is_real_file(&self) -> bool {
226 | matches!(self, Self::Filesystem(_))
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Andrew Radev
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "aho-corasick"
7 | version = "1.1.3"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
10 | dependencies = [
11 | "memchr",
12 | ]
13 |
14 | [[package]]
15 | name = "anyhow"
16 | version = "1.0.96"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
19 |
20 | [[package]]
21 | name = "arrayref"
22 | version = "0.3.9"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
25 |
26 | [[package]]
27 | name = "arrayvec"
28 | version = "0.5.2"
29 | source = "registry+https://github.com/rust-lang/crates.io-index"
30 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
31 |
32 | [[package]]
33 | name = "atk"
34 | version = "0.15.1"
35 | source = "registry+https://github.com/rust-lang/crates.io-index"
36 | checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd"
37 | dependencies = [
38 | "atk-sys",
39 | "bitflags 1.3.2",
40 | "glib",
41 | "libc",
42 | ]
43 |
44 | [[package]]
45 | name = "atk-sys"
46 | version = "0.15.1"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6"
49 | dependencies = [
50 | "glib-sys",
51 | "gobject-sys",
52 | "libc",
53 | "system-deps 6.2.2",
54 | ]
55 |
56 | [[package]]
57 | name = "autocfg"
58 | version = "1.0.1"
59 | source = "registry+https://github.com/rust-lang/crates.io-index"
60 | checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
61 |
62 | [[package]]
63 | name = "base64"
64 | version = "0.13.1"
65 | source = "registry+https://github.com/rust-lang/crates.io-index"
66 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
67 |
68 | [[package]]
69 | name = "bitflags"
70 | version = "1.3.2"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
73 |
74 | [[package]]
75 | name = "bitflags"
76 | version = "2.8.0"
77 | source = "registry+https://github.com/rust-lang/crates.io-index"
78 | checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
79 |
80 | [[package]]
81 | name = "blake2b_simd"
82 | version = "0.5.11"
83 | source = "registry+https://github.com/rust-lang/crates.io-index"
84 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
85 | dependencies = [
86 | "arrayref",
87 | "arrayvec",
88 | "constant_time_eq",
89 | ]
90 |
91 | [[package]]
92 | name = "cairo-rs"
93 | version = "0.15.12"
94 | source = "registry+https://github.com/rust-lang/crates.io-index"
95 | checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc"
96 | dependencies = [
97 | "bitflags 1.3.2",
98 | "cairo-sys-rs",
99 | "glib",
100 | "libc",
101 | "thiserror",
102 | ]
103 |
104 | [[package]]
105 | name = "cairo-sys-rs"
106 | version = "0.15.1"
107 | source = "registry+https://github.com/rust-lang/crates.io-index"
108 | checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8"
109 | dependencies = [
110 | "glib-sys",
111 | "libc",
112 | "system-deps 6.2.2",
113 | ]
114 |
115 | [[package]]
116 | name = "cfg-expr"
117 | version = "0.9.1"
118 | source = "registry+https://github.com/rust-lang/crates.io-index"
119 | checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7"
120 | dependencies = [
121 | "smallvec",
122 | ]
123 |
124 | [[package]]
125 | name = "cfg-expr"
126 | version = "0.15.8"
127 | source = "registry+https://github.com/rust-lang/crates.io-index"
128 | checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
129 | dependencies = [
130 | "smallvec",
131 | "target-lexicon",
132 | ]
133 |
134 | [[package]]
135 | name = "cfg-if"
136 | version = "0.1.10"
137 | source = "registry+https://github.com/rust-lang/crates.io-index"
138 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
139 |
140 | [[package]]
141 | name = "cfg-if"
142 | version = "1.0.0"
143 | source = "registry+https://github.com/rust-lang/crates.io-index"
144 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
145 |
146 | [[package]]
147 | name = "claim"
148 | version = "0.5.0"
149 | source = "registry+https://github.com/rust-lang/crates.io-index"
150 | checksum = "f81099d6bb72e1df6d50bb2347224b666a670912bb7f06dbe867a4a070ab3ce8"
151 | dependencies = [
152 | "autocfg",
153 | ]
154 |
155 | [[package]]
156 | name = "clap"
157 | version = "2.34.0"
158 | source = "registry+https://github.com/rust-lang/crates.io-index"
159 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
160 | dependencies = [
161 | "bitflags 1.3.2",
162 | "textwrap",
163 | "unicode-width",
164 | ]
165 |
166 | [[package]]
167 | name = "constant_time_eq"
168 | version = "0.1.5"
169 | source = "registry+https://github.com/rust-lang/crates.io-index"
170 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
171 |
172 | [[package]]
173 | name = "crossbeam-utils"
174 | version = "0.8.21"
175 | source = "registry+https://github.com/rust-lang/crates.io-index"
176 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
177 |
178 | [[package]]
179 | name = "directories"
180 | version = "4.0.1"
181 | source = "registry+https://github.com/rust-lang/crates.io-index"
182 | checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
183 | dependencies = [
184 | "dirs-sys",
185 | ]
186 |
187 | [[package]]
188 | name = "dirs"
189 | version = "1.0.5"
190 | source = "registry+https://github.com/rust-lang/crates.io-index"
191 | checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
192 | dependencies = [
193 | "libc",
194 | "redox_users 0.3.5",
195 | "winapi 0.3.9",
196 | ]
197 |
198 | [[package]]
199 | name = "dirs-sys"
200 | version = "0.3.7"
201 | source = "registry+https://github.com/rust-lang/crates.io-index"
202 | checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
203 | dependencies = [
204 | "libc",
205 | "redox_users 0.4.6",
206 | "winapi 0.3.9",
207 | ]
208 |
209 | [[package]]
210 | name = "env_logger"
211 | version = "0.10.2"
212 | source = "registry+https://github.com/rust-lang/crates.io-index"
213 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
214 | dependencies = [
215 | "log",
216 | ]
217 |
218 | [[package]]
219 | name = "equivalent"
220 | version = "1.0.2"
221 | source = "registry+https://github.com/rust-lang/crates.io-index"
222 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
223 |
224 | [[package]]
225 | name = "errno"
226 | version = "0.3.10"
227 | source = "registry+https://github.com/rust-lang/crates.io-index"
228 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
229 | dependencies = [
230 | "libc",
231 | "windows-sys",
232 | ]
233 |
234 | [[package]]
235 | name = "fastrand"
236 | version = "2.3.0"
237 | source = "registry+https://github.com/rust-lang/crates.io-index"
238 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
239 |
240 | [[package]]
241 | name = "field-offset"
242 | version = "0.3.6"
243 | source = "registry+https://github.com/rust-lang/crates.io-index"
244 | checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
245 | dependencies = [
246 | "memoffset",
247 | "rustc_version",
248 | ]
249 |
250 | [[package]]
251 | name = "filetime"
252 | version = "0.2.25"
253 | source = "registry+https://github.com/rust-lang/crates.io-index"
254 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
255 | dependencies = [
256 | "cfg-if 1.0.0",
257 | "libc",
258 | "libredox",
259 | "windows-sys",
260 | ]
261 |
262 | [[package]]
263 | name = "fsevent"
264 | version = "0.4.0"
265 | source = "registry+https://github.com/rust-lang/crates.io-index"
266 | checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
267 | dependencies = [
268 | "bitflags 1.3.2",
269 | "fsevent-sys",
270 | ]
271 |
272 | [[package]]
273 | name = "fsevent-sys"
274 | version = "2.0.1"
275 | source = "registry+https://github.com/rust-lang/crates.io-index"
276 | checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
277 | dependencies = [
278 | "libc",
279 | ]
280 |
281 | [[package]]
282 | name = "fuchsia-zircon"
283 | version = "0.3.3"
284 | source = "registry+https://github.com/rust-lang/crates.io-index"
285 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
286 | dependencies = [
287 | "bitflags 1.3.2",
288 | "fuchsia-zircon-sys",
289 | ]
290 |
291 | [[package]]
292 | name = "fuchsia-zircon-sys"
293 | version = "0.3.3"
294 | source = "registry+https://github.com/rust-lang/crates.io-index"
295 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
296 |
297 | [[package]]
298 | name = "futures-channel"
299 | version = "0.3.31"
300 | source = "registry+https://github.com/rust-lang/crates.io-index"
301 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
302 | dependencies = [
303 | "futures-core",
304 | ]
305 |
306 | [[package]]
307 | name = "futures-core"
308 | version = "0.3.31"
309 | source = "registry+https://github.com/rust-lang/crates.io-index"
310 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
311 |
312 | [[package]]
313 | name = "futures-executor"
314 | version = "0.3.31"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
317 | dependencies = [
318 | "futures-core",
319 | "futures-task",
320 | "futures-util",
321 | ]
322 |
323 | [[package]]
324 | name = "futures-io"
325 | version = "0.3.31"
326 | source = "registry+https://github.com/rust-lang/crates.io-index"
327 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
328 |
329 | [[package]]
330 | name = "futures-task"
331 | version = "0.3.31"
332 | source = "registry+https://github.com/rust-lang/crates.io-index"
333 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
334 |
335 | [[package]]
336 | name = "futures-util"
337 | version = "0.3.31"
338 | source = "registry+https://github.com/rust-lang/crates.io-index"
339 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
340 | dependencies = [
341 | "futures-core",
342 | "futures-task",
343 | "pin-project-lite",
344 | "pin-utils",
345 | "slab",
346 | ]
347 |
348 | [[package]]
349 | name = "gdk"
350 | version = "0.15.4"
351 | source = "registry+https://github.com/rust-lang/crates.io-index"
352 | checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8"
353 | dependencies = [
354 | "bitflags 1.3.2",
355 | "cairo-rs",
356 | "gdk-pixbuf",
357 | "gdk-sys",
358 | "gio",
359 | "glib",
360 | "libc",
361 | "pango",
362 | ]
363 |
364 | [[package]]
365 | name = "gdk-pixbuf"
366 | version = "0.15.11"
367 | source = "registry+https://github.com/rust-lang/crates.io-index"
368 | checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a"
369 | dependencies = [
370 | "bitflags 1.3.2",
371 | "gdk-pixbuf-sys",
372 | "gio",
373 | "glib",
374 | "libc",
375 | ]
376 |
377 | [[package]]
378 | name = "gdk-pixbuf-sys"
379 | version = "0.15.10"
380 | source = "registry+https://github.com/rust-lang/crates.io-index"
381 | checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7"
382 | dependencies = [
383 | "gio-sys",
384 | "glib-sys",
385 | "gobject-sys",
386 | "libc",
387 | "system-deps 6.2.2",
388 | ]
389 |
390 | [[package]]
391 | name = "gdk-sys"
392 | version = "0.15.1"
393 | source = "registry+https://github.com/rust-lang/crates.io-index"
394 | checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88"
395 | dependencies = [
396 | "cairo-sys-rs",
397 | "gdk-pixbuf-sys",
398 | "gio-sys",
399 | "glib-sys",
400 | "gobject-sys",
401 | "libc",
402 | "pango-sys",
403 | "pkg-config",
404 | "system-deps 6.2.2",
405 | ]
406 |
407 | [[package]]
408 | name = "getrandom"
409 | version = "0.1.16"
410 | source = "registry+https://github.com/rust-lang/crates.io-index"
411 | checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
412 | dependencies = [
413 | "cfg-if 1.0.0",
414 | "libc",
415 | "wasi 0.9.0+wasi-snapshot-preview1",
416 | ]
417 |
418 | [[package]]
419 | name = "getrandom"
420 | version = "0.2.15"
421 | source = "registry+https://github.com/rust-lang/crates.io-index"
422 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
423 | dependencies = [
424 | "cfg-if 1.0.0",
425 | "libc",
426 | "wasi 0.11.0+wasi-snapshot-preview1",
427 | ]
428 |
429 | [[package]]
430 | name = "getrandom"
431 | version = "0.3.1"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
434 | dependencies = [
435 | "cfg-if 1.0.0",
436 | "libc",
437 | "wasi 0.13.3+wasi-0.2.2",
438 | "windows-targets",
439 | ]
440 |
441 | [[package]]
442 | name = "gio"
443 | version = "0.15.12"
444 | source = "registry+https://github.com/rust-lang/crates.io-index"
445 | checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b"
446 | dependencies = [
447 | "bitflags 1.3.2",
448 | "futures-channel",
449 | "futures-core",
450 | "futures-io",
451 | "gio-sys",
452 | "glib",
453 | "libc",
454 | "once_cell",
455 | "thiserror",
456 | ]
457 |
458 | [[package]]
459 | name = "gio-sys"
460 | version = "0.15.10"
461 | source = "registry+https://github.com/rust-lang/crates.io-index"
462 | checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d"
463 | dependencies = [
464 | "glib-sys",
465 | "gobject-sys",
466 | "libc",
467 | "system-deps 6.2.2",
468 | "winapi 0.3.9",
469 | ]
470 |
471 | [[package]]
472 | name = "glib"
473 | version = "0.15.12"
474 | source = "registry+https://github.com/rust-lang/crates.io-index"
475 | checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d"
476 | dependencies = [
477 | "bitflags 1.3.2",
478 | "futures-channel",
479 | "futures-core",
480 | "futures-executor",
481 | "futures-task",
482 | "glib-macros",
483 | "glib-sys",
484 | "gobject-sys",
485 | "libc",
486 | "once_cell",
487 | "smallvec",
488 | "thiserror",
489 | ]
490 |
491 | [[package]]
492 | name = "glib-macros"
493 | version = "0.15.13"
494 | source = "registry+https://github.com/rust-lang/crates.io-index"
495 | checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a"
496 | dependencies = [
497 | "anyhow",
498 | "heck 0.4.1",
499 | "proc-macro-crate",
500 | "proc-macro-error",
501 | "proc-macro2",
502 | "quote",
503 | "syn 1.0.109",
504 | ]
505 |
506 | [[package]]
507 | name = "glib-sys"
508 | version = "0.15.10"
509 | source = "registry+https://github.com/rust-lang/crates.io-index"
510 | checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4"
511 | dependencies = [
512 | "libc",
513 | "system-deps 6.2.2",
514 | ]
515 |
516 | [[package]]
517 | name = "gobject-sys"
518 | version = "0.15.10"
519 | source = "registry+https://github.com/rust-lang/crates.io-index"
520 | checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a"
521 | dependencies = [
522 | "glib-sys",
523 | "libc",
524 | "system-deps 6.2.2",
525 | ]
526 |
527 | [[package]]
528 | name = "gtk"
529 | version = "0.15.5"
530 | source = "registry+https://github.com/rust-lang/crates.io-index"
531 | checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0"
532 | dependencies = [
533 | "atk",
534 | "bitflags 1.3.2",
535 | "cairo-rs",
536 | "field-offset",
537 | "futures-channel",
538 | "gdk",
539 | "gdk-pixbuf",
540 | "gio",
541 | "glib",
542 | "gtk-sys",
543 | "gtk3-macros",
544 | "libc",
545 | "once_cell",
546 | "pango",
547 | "pkg-config",
548 | ]
549 |
550 | [[package]]
551 | name = "gtk-sys"
552 | version = "0.15.3"
553 | source = "registry+https://github.com/rust-lang/crates.io-index"
554 | checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84"
555 | dependencies = [
556 | "atk-sys",
557 | "cairo-sys-rs",
558 | "gdk-pixbuf-sys",
559 | "gdk-sys",
560 | "gio-sys",
561 | "glib-sys",
562 | "gobject-sys",
563 | "libc",
564 | "pango-sys",
565 | "system-deps 6.2.2",
566 | ]
567 |
568 | [[package]]
569 | name = "gtk3-macros"
570 | version = "0.15.6"
571 | source = "registry+https://github.com/rust-lang/crates.io-index"
572 | checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d"
573 | dependencies = [
574 | "anyhow",
575 | "proc-macro-crate",
576 | "proc-macro-error",
577 | "proc-macro2",
578 | "quote",
579 | "syn 1.0.109",
580 | ]
581 |
582 | [[package]]
583 | name = "hashbrown"
584 | version = "0.15.2"
585 | source = "registry+https://github.com/rust-lang/crates.io-index"
586 | checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
587 |
588 | [[package]]
589 | name = "heck"
590 | version = "0.3.3"
591 | source = "registry+https://github.com/rust-lang/crates.io-index"
592 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
593 | dependencies = [
594 | "unicode-segmentation",
595 | ]
596 |
597 | [[package]]
598 | name = "heck"
599 | version = "0.4.1"
600 | source = "registry+https://github.com/rust-lang/crates.io-index"
601 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
602 |
603 | [[package]]
604 | name = "heck"
605 | version = "0.5.0"
606 | source = "registry+https://github.com/rust-lang/crates.io-index"
607 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
608 |
609 | [[package]]
610 | name = "indexmap"
611 | version = "2.7.1"
612 | source = "registry+https://github.com/rust-lang/crates.io-index"
613 | checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
614 | dependencies = [
615 | "equivalent",
616 | "hashbrown",
617 | ]
618 |
619 | [[package]]
620 | name = "inotify"
621 | version = "0.7.1"
622 | source = "registry+https://github.com/rust-lang/crates.io-index"
623 | checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
624 | dependencies = [
625 | "bitflags 1.3.2",
626 | "inotify-sys",
627 | "libc",
628 | ]
629 |
630 | [[package]]
631 | name = "inotify-sys"
632 | version = "0.1.5"
633 | source = "registry+https://github.com/rust-lang/crates.io-index"
634 | checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
635 | dependencies = [
636 | "libc",
637 | ]
638 |
639 | [[package]]
640 | name = "iovec"
641 | version = "0.1.4"
642 | source = "registry+https://github.com/rust-lang/crates.io-index"
643 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
644 | dependencies = [
645 | "libc",
646 | ]
647 |
648 | [[package]]
649 | name = "itoa"
650 | version = "1.0.14"
651 | source = "registry+https://github.com/rust-lang/crates.io-index"
652 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
653 |
654 | [[package]]
655 | name = "javascriptcore-rs"
656 | version = "0.16.0"
657 | source = "registry+https://github.com/rust-lang/crates.io-index"
658 | checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c"
659 | dependencies = [
660 | "bitflags 1.3.2",
661 | "glib",
662 | "javascriptcore-rs-sys",
663 | ]
664 |
665 | [[package]]
666 | name = "javascriptcore-rs-sys"
667 | version = "0.4.0"
668 | source = "registry+https://github.com/rust-lang/crates.io-index"
669 | checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c"
670 | dependencies = [
671 | "glib-sys",
672 | "gobject-sys",
673 | "libc",
674 | "system-deps 5.0.0",
675 | ]
676 |
677 | [[package]]
678 | name = "kernel32-sys"
679 | version = "0.2.2"
680 | source = "registry+https://github.com/rust-lang/crates.io-index"
681 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
682 | dependencies = [
683 | "winapi 0.2.8",
684 | "winapi-build",
685 | ]
686 |
687 | [[package]]
688 | name = "lazy_static"
689 | version = "1.5.0"
690 | source = "registry+https://github.com/rust-lang/crates.io-index"
691 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
692 |
693 | [[package]]
694 | name = "lazycell"
695 | version = "1.3.0"
696 | source = "registry+https://github.com/rust-lang/crates.io-index"
697 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
698 |
699 | [[package]]
700 | name = "libc"
701 | version = "0.2.169"
702 | source = "registry+https://github.com/rust-lang/crates.io-index"
703 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
704 |
705 | [[package]]
706 | name = "libredox"
707 | version = "0.1.3"
708 | source = "registry+https://github.com/rust-lang/crates.io-index"
709 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
710 | dependencies = [
711 | "bitflags 2.8.0",
712 | "libc",
713 | "redox_syscall 0.5.8",
714 | ]
715 |
716 | [[package]]
717 | name = "linux-raw-sys"
718 | version = "0.4.15"
719 | source = "registry+https://github.com/rust-lang/crates.io-index"
720 | checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
721 |
722 | [[package]]
723 | name = "log"
724 | version = "0.4.25"
725 | source = "registry+https://github.com/rust-lang/crates.io-index"
726 | checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
727 |
728 | [[package]]
729 | name = "memchr"
730 | version = "2.7.4"
731 | source = "registry+https://github.com/rust-lang/crates.io-index"
732 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
733 |
734 | [[package]]
735 | name = "memoffset"
736 | version = "0.9.1"
737 | source = "registry+https://github.com/rust-lang/crates.io-index"
738 | checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
739 | dependencies = [
740 | "autocfg",
741 | ]
742 |
743 | [[package]]
744 | name = "mio"
745 | version = "0.6.23"
746 | source = "registry+https://github.com/rust-lang/crates.io-index"
747 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
748 | dependencies = [
749 | "cfg-if 0.1.10",
750 | "fuchsia-zircon",
751 | "fuchsia-zircon-sys",
752 | "iovec",
753 | "kernel32-sys",
754 | "libc",
755 | "log",
756 | "miow",
757 | "net2",
758 | "slab",
759 | "winapi 0.2.8",
760 | ]
761 |
762 | [[package]]
763 | name = "mio-extras"
764 | version = "2.0.6"
765 | source = "registry+https://github.com/rust-lang/crates.io-index"
766 | checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
767 | dependencies = [
768 | "lazycell",
769 | "log",
770 | "mio",
771 | "slab",
772 | ]
773 |
774 | [[package]]
775 | name = "miow"
776 | version = "0.2.2"
777 | source = "registry+https://github.com/rust-lang/crates.io-index"
778 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
779 | dependencies = [
780 | "kernel32-sys",
781 | "net2",
782 | "winapi 0.2.8",
783 | "ws2_32-sys",
784 | ]
785 |
786 | [[package]]
787 | name = "net2"
788 | version = "0.2.39"
789 | source = "registry+https://github.com/rust-lang/crates.io-index"
790 | checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac"
791 | dependencies = [
792 | "cfg-if 0.1.10",
793 | "libc",
794 | "winapi 0.3.9",
795 | ]
796 |
797 | [[package]]
798 | name = "notify"
799 | version = "4.0.18"
800 | source = "registry+https://github.com/rust-lang/crates.io-index"
801 | checksum = "b72dd35279a5dc895a30965e247b0961ba36c233dc48454a2de8ccd459f1afd3"
802 | dependencies = [
803 | "bitflags 1.3.2",
804 | "filetime",
805 | "fsevent",
806 | "fsevent-sys",
807 | "inotify",
808 | "libc",
809 | "mio",
810 | "mio-extras",
811 | "walkdir",
812 | "winapi 0.3.9",
813 | ]
814 |
815 | [[package]]
816 | name = "once_cell"
817 | version = "1.20.3"
818 | source = "registry+https://github.com/rust-lang/crates.io-index"
819 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
820 |
821 | [[package]]
822 | name = "pango"
823 | version = "0.15.10"
824 | source = "registry+https://github.com/rust-lang/crates.io-index"
825 | checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f"
826 | dependencies = [
827 | "bitflags 1.3.2",
828 | "glib",
829 | "libc",
830 | "once_cell",
831 | "pango-sys",
832 | ]
833 |
834 | [[package]]
835 | name = "pango-sys"
836 | version = "0.15.10"
837 | source = "registry+https://github.com/rust-lang/crates.io-index"
838 | checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa"
839 | dependencies = [
840 | "glib-sys",
841 | "gobject-sys",
842 | "libc",
843 | "system-deps 6.2.2",
844 | ]
845 |
846 | [[package]]
847 | name = "pathbuftools"
848 | version = "0.1.2"
849 | source = "registry+https://github.com/rust-lang/crates.io-index"
850 | checksum = "00f002a88874c85e30d4e133baae43cf382ceecf025f9493ce23eb381fcc922e"
851 | dependencies = [
852 | "dirs",
853 | ]
854 |
855 | [[package]]
856 | name = "pin-project-lite"
857 | version = "0.2.16"
858 | source = "registry+https://github.com/rust-lang/crates.io-index"
859 | checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
860 |
861 | [[package]]
862 | name = "pin-utils"
863 | version = "0.1.0"
864 | source = "registry+https://github.com/rust-lang/crates.io-index"
865 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
866 |
867 | [[package]]
868 | name = "pkg-config"
869 | version = "0.3.31"
870 | source = "registry+https://github.com/rust-lang/crates.io-index"
871 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
872 |
873 | [[package]]
874 | name = "proc-macro-crate"
875 | version = "1.3.1"
876 | source = "registry+https://github.com/rust-lang/crates.io-index"
877 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
878 | dependencies = [
879 | "once_cell",
880 | "toml_edit 0.19.15",
881 | ]
882 |
883 | [[package]]
884 | name = "proc-macro-error"
885 | version = "1.0.4"
886 | source = "registry+https://github.com/rust-lang/crates.io-index"
887 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
888 | dependencies = [
889 | "proc-macro-error-attr",
890 | "proc-macro2",
891 | "quote",
892 | "syn 1.0.109",
893 | "version_check",
894 | ]
895 |
896 | [[package]]
897 | name = "proc-macro-error-attr"
898 | version = "1.0.4"
899 | source = "registry+https://github.com/rust-lang/crates.io-index"
900 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
901 | dependencies = [
902 | "proc-macro2",
903 | "quote",
904 | "version_check",
905 | ]
906 |
907 | [[package]]
908 | name = "proc-macro2"
909 | version = "1.0.93"
910 | source = "registry+https://github.com/rust-lang/crates.io-index"
911 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
912 | dependencies = [
913 | "unicode-ident",
914 | ]
915 |
916 | [[package]]
917 | name = "pulldown-cmark"
918 | version = "0.9.6"
919 | source = "registry+https://github.com/rust-lang/crates.io-index"
920 | checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
921 | dependencies = [
922 | "bitflags 2.8.0",
923 | "memchr",
924 | "unicase",
925 | ]
926 |
927 | [[package]]
928 | name = "quickmd"
929 | version = "0.7.1"
930 | dependencies = [
931 | "anyhow",
932 | "claim",
933 | "directories",
934 | "env_logger",
935 | "gdk",
936 | "gdk-pixbuf",
937 | "gio",
938 | "glib",
939 | "gtk",
940 | "log",
941 | "notify",
942 | "pathbuftools",
943 | "pulldown-cmark",
944 | "regex",
945 | "serde",
946 | "serde_json",
947 | "serde_yaml",
948 | "structopt",
949 | "tempfile",
950 | "webkit2gtk",
951 | ]
952 |
953 | [[package]]
954 | name = "quote"
955 | version = "1.0.38"
956 | source = "registry+https://github.com/rust-lang/crates.io-index"
957 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
958 | dependencies = [
959 | "proc-macro2",
960 | ]
961 |
962 | [[package]]
963 | name = "redox_syscall"
964 | version = "0.1.57"
965 | source = "registry+https://github.com/rust-lang/crates.io-index"
966 | checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
967 |
968 | [[package]]
969 | name = "redox_syscall"
970 | version = "0.5.8"
971 | source = "registry+https://github.com/rust-lang/crates.io-index"
972 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
973 | dependencies = [
974 | "bitflags 2.8.0",
975 | ]
976 |
977 | [[package]]
978 | name = "redox_users"
979 | version = "0.3.5"
980 | source = "registry+https://github.com/rust-lang/crates.io-index"
981 | checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
982 | dependencies = [
983 | "getrandom 0.1.16",
984 | "redox_syscall 0.1.57",
985 | "rust-argon2",
986 | ]
987 |
988 | [[package]]
989 | name = "redox_users"
990 | version = "0.4.6"
991 | source = "registry+https://github.com/rust-lang/crates.io-index"
992 | checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
993 | dependencies = [
994 | "getrandom 0.2.15",
995 | "libredox",
996 | "thiserror",
997 | ]
998 |
999 | [[package]]
1000 | name = "regex"
1001 | version = "1.11.1"
1002 | source = "registry+https://github.com/rust-lang/crates.io-index"
1003 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
1004 | dependencies = [
1005 | "aho-corasick",
1006 | "memchr",
1007 | "regex-automata",
1008 | "regex-syntax",
1009 | ]
1010 |
1011 | [[package]]
1012 | name = "regex-automata"
1013 | version = "0.4.9"
1014 | source = "registry+https://github.com/rust-lang/crates.io-index"
1015 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
1016 | dependencies = [
1017 | "aho-corasick",
1018 | "memchr",
1019 | "regex-syntax",
1020 | ]
1021 |
1022 | [[package]]
1023 | name = "regex-syntax"
1024 | version = "0.8.5"
1025 | source = "registry+https://github.com/rust-lang/crates.io-index"
1026 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
1027 |
1028 | [[package]]
1029 | name = "rust-argon2"
1030 | version = "0.8.3"
1031 | source = "registry+https://github.com/rust-lang/crates.io-index"
1032 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb"
1033 | dependencies = [
1034 | "base64",
1035 | "blake2b_simd",
1036 | "constant_time_eq",
1037 | "crossbeam-utils",
1038 | ]
1039 |
1040 | [[package]]
1041 | name = "rustc_version"
1042 | version = "0.4.1"
1043 | source = "registry+https://github.com/rust-lang/crates.io-index"
1044 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1045 | dependencies = [
1046 | "semver",
1047 | ]
1048 |
1049 | [[package]]
1050 | name = "rustix"
1051 | version = "0.38.44"
1052 | source = "registry+https://github.com/rust-lang/crates.io-index"
1053 | checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
1054 | dependencies = [
1055 | "bitflags 2.8.0",
1056 | "errno",
1057 | "libc",
1058 | "linux-raw-sys",
1059 | "windows-sys",
1060 | ]
1061 |
1062 | [[package]]
1063 | name = "ryu"
1064 | version = "1.0.19"
1065 | source = "registry+https://github.com/rust-lang/crates.io-index"
1066 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
1067 |
1068 | [[package]]
1069 | name = "same-file"
1070 | version = "1.0.6"
1071 | source = "registry+https://github.com/rust-lang/crates.io-index"
1072 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1073 | dependencies = [
1074 | "winapi-util",
1075 | ]
1076 |
1077 | [[package]]
1078 | name = "semver"
1079 | version = "1.0.25"
1080 | source = "registry+https://github.com/rust-lang/crates.io-index"
1081 | checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03"
1082 |
1083 | [[package]]
1084 | name = "serde"
1085 | version = "1.0.218"
1086 | source = "registry+https://github.com/rust-lang/crates.io-index"
1087 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
1088 | dependencies = [
1089 | "serde_derive",
1090 | ]
1091 |
1092 | [[package]]
1093 | name = "serde_derive"
1094 | version = "1.0.218"
1095 | source = "registry+https://github.com/rust-lang/crates.io-index"
1096 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
1097 | dependencies = [
1098 | "proc-macro2",
1099 | "quote",
1100 | "syn 2.0.98",
1101 | ]
1102 |
1103 | [[package]]
1104 | name = "serde_json"
1105 | version = "1.0.139"
1106 | source = "registry+https://github.com/rust-lang/crates.io-index"
1107 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
1108 | dependencies = [
1109 | "itoa",
1110 | "memchr",
1111 | "ryu",
1112 | "serde",
1113 | ]
1114 |
1115 | [[package]]
1116 | name = "serde_spanned"
1117 | version = "0.6.8"
1118 | source = "registry+https://github.com/rust-lang/crates.io-index"
1119 | checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
1120 | dependencies = [
1121 | "serde",
1122 | ]
1123 |
1124 | [[package]]
1125 | name = "serde_yaml"
1126 | version = "0.9.34+deprecated"
1127 | source = "registry+https://github.com/rust-lang/crates.io-index"
1128 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
1129 | dependencies = [
1130 | "indexmap",
1131 | "itoa",
1132 | "ryu",
1133 | "serde",
1134 | "unsafe-libyaml",
1135 | ]
1136 |
1137 | [[package]]
1138 | name = "slab"
1139 | version = "0.4.9"
1140 | source = "registry+https://github.com/rust-lang/crates.io-index"
1141 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
1142 | dependencies = [
1143 | "autocfg",
1144 | ]
1145 |
1146 | [[package]]
1147 | name = "smallvec"
1148 | version = "1.14.0"
1149 | source = "registry+https://github.com/rust-lang/crates.io-index"
1150 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
1151 |
1152 | [[package]]
1153 | name = "soup2"
1154 | version = "0.2.1"
1155 | source = "registry+https://github.com/rust-lang/crates.io-index"
1156 | checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0"
1157 | dependencies = [
1158 | "bitflags 1.3.2",
1159 | "gio",
1160 | "glib",
1161 | "libc",
1162 | "once_cell",
1163 | "soup2-sys",
1164 | ]
1165 |
1166 | [[package]]
1167 | name = "soup2-sys"
1168 | version = "0.2.0"
1169 | source = "registry+https://github.com/rust-lang/crates.io-index"
1170 | checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf"
1171 | dependencies = [
1172 | "bitflags 1.3.2",
1173 | "gio-sys",
1174 | "glib-sys",
1175 | "gobject-sys",
1176 | "libc",
1177 | "system-deps 5.0.0",
1178 | ]
1179 |
1180 | [[package]]
1181 | name = "structopt"
1182 | version = "0.3.26"
1183 | source = "registry+https://github.com/rust-lang/crates.io-index"
1184 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10"
1185 | dependencies = [
1186 | "clap",
1187 | "lazy_static",
1188 | "structopt-derive",
1189 | ]
1190 |
1191 | [[package]]
1192 | name = "structopt-derive"
1193 | version = "0.4.18"
1194 | source = "registry+https://github.com/rust-lang/crates.io-index"
1195 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0"
1196 | dependencies = [
1197 | "heck 0.3.3",
1198 | "proc-macro-error",
1199 | "proc-macro2",
1200 | "quote",
1201 | "syn 1.0.109",
1202 | ]
1203 |
1204 | [[package]]
1205 | name = "syn"
1206 | version = "1.0.109"
1207 | source = "registry+https://github.com/rust-lang/crates.io-index"
1208 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
1209 | dependencies = [
1210 | "proc-macro2",
1211 | "quote",
1212 | "unicode-ident",
1213 | ]
1214 |
1215 | [[package]]
1216 | name = "syn"
1217 | version = "2.0.98"
1218 | source = "registry+https://github.com/rust-lang/crates.io-index"
1219 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
1220 | dependencies = [
1221 | "proc-macro2",
1222 | "quote",
1223 | "unicode-ident",
1224 | ]
1225 |
1226 | [[package]]
1227 | name = "system-deps"
1228 | version = "5.0.0"
1229 | source = "registry+https://github.com/rust-lang/crates.io-index"
1230 | checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e"
1231 | dependencies = [
1232 | "cfg-expr 0.9.1",
1233 | "heck 0.3.3",
1234 | "pkg-config",
1235 | "toml 0.5.11",
1236 | "version-compare 0.0.11",
1237 | ]
1238 |
1239 | [[package]]
1240 | name = "system-deps"
1241 | version = "6.2.2"
1242 | source = "registry+https://github.com/rust-lang/crates.io-index"
1243 | checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
1244 | dependencies = [
1245 | "cfg-expr 0.15.8",
1246 | "heck 0.5.0",
1247 | "pkg-config",
1248 | "toml 0.8.20",
1249 | "version-compare 0.2.0",
1250 | ]
1251 |
1252 | [[package]]
1253 | name = "target-lexicon"
1254 | version = "0.12.16"
1255 | source = "registry+https://github.com/rust-lang/crates.io-index"
1256 | checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
1257 |
1258 | [[package]]
1259 | name = "tempfile"
1260 | version = "3.17.1"
1261 | source = "registry+https://github.com/rust-lang/crates.io-index"
1262 | checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
1263 | dependencies = [
1264 | "cfg-if 1.0.0",
1265 | "fastrand",
1266 | "getrandom 0.3.1",
1267 | "once_cell",
1268 | "rustix",
1269 | "windows-sys",
1270 | ]
1271 |
1272 | [[package]]
1273 | name = "textwrap"
1274 | version = "0.11.0"
1275 | source = "registry+https://github.com/rust-lang/crates.io-index"
1276 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
1277 | dependencies = [
1278 | "unicode-width",
1279 | ]
1280 |
1281 | [[package]]
1282 | name = "thiserror"
1283 | version = "1.0.69"
1284 | source = "registry+https://github.com/rust-lang/crates.io-index"
1285 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1286 | dependencies = [
1287 | "thiserror-impl",
1288 | ]
1289 |
1290 | [[package]]
1291 | name = "thiserror-impl"
1292 | version = "1.0.69"
1293 | source = "registry+https://github.com/rust-lang/crates.io-index"
1294 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1295 | dependencies = [
1296 | "proc-macro2",
1297 | "quote",
1298 | "syn 2.0.98",
1299 | ]
1300 |
1301 | [[package]]
1302 | name = "toml"
1303 | version = "0.5.11"
1304 | source = "registry+https://github.com/rust-lang/crates.io-index"
1305 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
1306 | dependencies = [
1307 | "serde",
1308 | ]
1309 |
1310 | [[package]]
1311 | name = "toml"
1312 | version = "0.8.20"
1313 | source = "registry+https://github.com/rust-lang/crates.io-index"
1314 | checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
1315 | dependencies = [
1316 | "serde",
1317 | "serde_spanned",
1318 | "toml_datetime",
1319 | "toml_edit 0.22.24",
1320 | ]
1321 |
1322 | [[package]]
1323 | name = "toml_datetime"
1324 | version = "0.6.8"
1325 | source = "registry+https://github.com/rust-lang/crates.io-index"
1326 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
1327 | dependencies = [
1328 | "serde",
1329 | ]
1330 |
1331 | [[package]]
1332 | name = "toml_edit"
1333 | version = "0.19.15"
1334 | source = "registry+https://github.com/rust-lang/crates.io-index"
1335 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
1336 | dependencies = [
1337 | "indexmap",
1338 | "toml_datetime",
1339 | "winnow 0.5.40",
1340 | ]
1341 |
1342 | [[package]]
1343 | name = "toml_edit"
1344 | version = "0.22.24"
1345 | source = "registry+https://github.com/rust-lang/crates.io-index"
1346 | checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
1347 | dependencies = [
1348 | "indexmap",
1349 | "serde",
1350 | "serde_spanned",
1351 | "toml_datetime",
1352 | "winnow 0.7.3",
1353 | ]
1354 |
1355 | [[package]]
1356 | name = "unicase"
1357 | version = "2.8.1"
1358 | source = "registry+https://github.com/rust-lang/crates.io-index"
1359 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
1360 |
1361 | [[package]]
1362 | name = "unicode-ident"
1363 | version = "1.0.17"
1364 | source = "registry+https://github.com/rust-lang/crates.io-index"
1365 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
1366 |
1367 | [[package]]
1368 | name = "unicode-segmentation"
1369 | version = "1.12.0"
1370 | source = "registry+https://github.com/rust-lang/crates.io-index"
1371 | checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
1372 |
1373 | [[package]]
1374 | name = "unicode-width"
1375 | version = "0.1.14"
1376 | source = "registry+https://github.com/rust-lang/crates.io-index"
1377 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
1378 |
1379 | [[package]]
1380 | name = "unsafe-libyaml"
1381 | version = "0.2.11"
1382 | source = "registry+https://github.com/rust-lang/crates.io-index"
1383 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
1384 |
1385 | [[package]]
1386 | name = "version-compare"
1387 | version = "0.0.11"
1388 | source = "registry+https://github.com/rust-lang/crates.io-index"
1389 | checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b"
1390 |
1391 | [[package]]
1392 | name = "version-compare"
1393 | version = "0.2.0"
1394 | source = "registry+https://github.com/rust-lang/crates.io-index"
1395 | checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
1396 |
1397 | [[package]]
1398 | name = "version_check"
1399 | version = "0.9.5"
1400 | source = "registry+https://github.com/rust-lang/crates.io-index"
1401 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1402 |
1403 | [[package]]
1404 | name = "walkdir"
1405 | version = "2.5.0"
1406 | source = "registry+https://github.com/rust-lang/crates.io-index"
1407 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1408 | dependencies = [
1409 | "same-file",
1410 | "winapi-util",
1411 | ]
1412 |
1413 | [[package]]
1414 | name = "wasi"
1415 | version = "0.9.0+wasi-snapshot-preview1"
1416 | source = "registry+https://github.com/rust-lang/crates.io-index"
1417 | checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
1418 |
1419 | [[package]]
1420 | name = "wasi"
1421 | version = "0.11.0+wasi-snapshot-preview1"
1422 | source = "registry+https://github.com/rust-lang/crates.io-index"
1423 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1424 |
1425 | [[package]]
1426 | name = "wasi"
1427 | version = "0.13.3+wasi-0.2.2"
1428 | source = "registry+https://github.com/rust-lang/crates.io-index"
1429 | checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
1430 | dependencies = [
1431 | "wit-bindgen-rt",
1432 | ]
1433 |
1434 | [[package]]
1435 | name = "webkit2gtk"
1436 | version = "0.18.2"
1437 | source = "registry+https://github.com/rust-lang/crates.io-index"
1438 | checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370"
1439 | dependencies = [
1440 | "bitflags 1.3.2",
1441 | "cairo-rs",
1442 | "gdk",
1443 | "gdk-sys",
1444 | "gio",
1445 | "gio-sys",
1446 | "glib",
1447 | "glib-sys",
1448 | "gobject-sys",
1449 | "gtk",
1450 | "gtk-sys",
1451 | "javascriptcore-rs",
1452 | "libc",
1453 | "once_cell",
1454 | "soup2",
1455 | "webkit2gtk-sys",
1456 | ]
1457 |
1458 | [[package]]
1459 | name = "webkit2gtk-sys"
1460 | version = "0.18.0"
1461 | source = "registry+https://github.com/rust-lang/crates.io-index"
1462 | checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3"
1463 | dependencies = [
1464 | "atk-sys",
1465 | "bitflags 1.3.2",
1466 | "cairo-sys-rs",
1467 | "gdk-pixbuf-sys",
1468 | "gdk-sys",
1469 | "gio-sys",
1470 | "glib-sys",
1471 | "gobject-sys",
1472 | "gtk-sys",
1473 | "javascriptcore-rs-sys",
1474 | "libc",
1475 | "pango-sys",
1476 | "pkg-config",
1477 | "soup2-sys",
1478 | "system-deps 6.2.2",
1479 | ]
1480 |
1481 | [[package]]
1482 | name = "winapi"
1483 | version = "0.2.8"
1484 | source = "registry+https://github.com/rust-lang/crates.io-index"
1485 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
1486 |
1487 | [[package]]
1488 | name = "winapi"
1489 | version = "0.3.9"
1490 | source = "registry+https://github.com/rust-lang/crates.io-index"
1491 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
1492 | dependencies = [
1493 | "winapi-i686-pc-windows-gnu",
1494 | "winapi-x86_64-pc-windows-gnu",
1495 | ]
1496 |
1497 | [[package]]
1498 | name = "winapi-build"
1499 | version = "0.1.1"
1500 | source = "registry+https://github.com/rust-lang/crates.io-index"
1501 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
1502 |
1503 | [[package]]
1504 | name = "winapi-i686-pc-windows-gnu"
1505 | version = "0.4.0"
1506 | source = "registry+https://github.com/rust-lang/crates.io-index"
1507 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
1508 |
1509 | [[package]]
1510 | name = "winapi-util"
1511 | version = "0.1.9"
1512 | source = "registry+https://github.com/rust-lang/crates.io-index"
1513 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
1514 | dependencies = [
1515 | "windows-sys",
1516 | ]
1517 |
1518 | [[package]]
1519 | name = "winapi-x86_64-pc-windows-gnu"
1520 | version = "0.4.0"
1521 | source = "registry+https://github.com/rust-lang/crates.io-index"
1522 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
1523 |
1524 | [[package]]
1525 | name = "windows-sys"
1526 | version = "0.59.0"
1527 | source = "registry+https://github.com/rust-lang/crates.io-index"
1528 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1529 | dependencies = [
1530 | "windows-targets",
1531 | ]
1532 |
1533 | [[package]]
1534 | name = "windows-targets"
1535 | version = "0.52.6"
1536 | source = "registry+https://github.com/rust-lang/crates.io-index"
1537 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
1538 | dependencies = [
1539 | "windows_aarch64_gnullvm",
1540 | "windows_aarch64_msvc",
1541 | "windows_i686_gnu",
1542 | "windows_i686_gnullvm",
1543 | "windows_i686_msvc",
1544 | "windows_x86_64_gnu",
1545 | "windows_x86_64_gnullvm",
1546 | "windows_x86_64_msvc",
1547 | ]
1548 |
1549 | [[package]]
1550 | name = "windows_aarch64_gnullvm"
1551 | version = "0.52.6"
1552 | source = "registry+https://github.com/rust-lang/crates.io-index"
1553 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
1554 |
1555 | [[package]]
1556 | name = "windows_aarch64_msvc"
1557 | version = "0.52.6"
1558 | source = "registry+https://github.com/rust-lang/crates.io-index"
1559 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
1560 |
1561 | [[package]]
1562 | name = "windows_i686_gnu"
1563 | version = "0.52.6"
1564 | source = "registry+https://github.com/rust-lang/crates.io-index"
1565 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
1566 |
1567 | [[package]]
1568 | name = "windows_i686_gnullvm"
1569 | version = "0.52.6"
1570 | source = "registry+https://github.com/rust-lang/crates.io-index"
1571 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
1572 |
1573 | [[package]]
1574 | name = "windows_i686_msvc"
1575 | version = "0.52.6"
1576 | source = "registry+https://github.com/rust-lang/crates.io-index"
1577 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
1578 |
1579 | [[package]]
1580 | name = "windows_x86_64_gnu"
1581 | version = "0.52.6"
1582 | source = "registry+https://github.com/rust-lang/crates.io-index"
1583 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
1584 |
1585 | [[package]]
1586 | name = "windows_x86_64_gnullvm"
1587 | version = "0.52.6"
1588 | source = "registry+https://github.com/rust-lang/crates.io-index"
1589 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
1590 |
1591 | [[package]]
1592 | name = "windows_x86_64_msvc"
1593 | version = "0.52.6"
1594 | source = "registry+https://github.com/rust-lang/crates.io-index"
1595 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1596 |
1597 | [[package]]
1598 | name = "winnow"
1599 | version = "0.5.40"
1600 | source = "registry+https://github.com/rust-lang/crates.io-index"
1601 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
1602 | dependencies = [
1603 | "memchr",
1604 | ]
1605 |
1606 | [[package]]
1607 | name = "winnow"
1608 | version = "0.7.3"
1609 | source = "registry+https://github.com/rust-lang/crates.io-index"
1610 | checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
1611 | dependencies = [
1612 | "memchr",
1613 | ]
1614 |
1615 | [[package]]
1616 | name = "wit-bindgen-rt"
1617 | version = "0.33.0"
1618 | source = "registry+https://github.com/rust-lang/crates.io-index"
1619 | checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
1620 | dependencies = [
1621 | "bitflags 2.8.0",
1622 | ]
1623 |
1624 | [[package]]
1625 | name = "ws2_32-sys"
1626 | version = "0.2.1"
1627 | source = "registry+https://github.com/rust-lang/crates.io-index"
1628 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
1629 | dependencies = [
1630 | "winapi 0.2.8",
1631 | "winapi-build",
1632 | ]
1633 |
--------------------------------------------------------------------------------