Commit 25930f61 authored by Michael Aaron Murphy's avatar Michael Aaron Murphy
Browse files

Initial Commit

parents
/target/
**/*.rs.bk
This diff is collapsed.
[package]
authors = ["Michael Aaron Murphy <mmstickman@gmail.com>"]
name = "fontfinder"
version = "1.0.0"
[[bin]]
name = "fontfinder"
path = "src/bin/mod.rs"
[dependencies]
horrorshow = "0.6.2"
lazy_static = "0.2"
reqwest = "0.8"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
[dependencies.derive-fail]
git = "https://github.com/withoutboats/derive-fail"
[dependencies.failure]
git = "https://github.com/withoutboats/failure"
[dependencies.gio]
git = "https://github.com/gtk-rs/gio.git"
[dependencies.gtk]
features = ["v3_22"]
git = "https://github.com/gtk-rs/gtk.git"
[dependencies.webkit2gtk]
features = ["v2_16"]
git = "https://github.com/gtk-rs/webkit2gtk-rs.git"
[lib]
path = "src/lib/mod.rs"
The MIT License (MIT)
Copyright (c) 2017 Michael Aaron Murphy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Font Finder
Have you RIIR lately? This project is a clean Rust implementation of
[TypeCatcher](https://github.com/andrewsomething/typecatcher/), which took
about two days. It is a GTK3 application for browsing through and installing
fonts from [Google's font archive](https://fonts.google.com/) from the comfort
of your Linux desktop. Compared to TypeCatcher, which is written in Python,
Font Finder also enables the ability to filter fonts by their categories,
has zero Python runtime dependencies, and has much better performance &
resource consumption.
## Installation Instructions
```
cargo install --git https://github.com/mmstick/fontfinder
```
> When a new suite of GTK crates are released that also contains the
> webkit2gtk crate, the above installation instructions will be changed to
> `cargo install fontfinder`.
## Screenshots
### Default
![First Screenshot](screenshot01.png)
### Filtering Fonts w/ Search
![Second Screenshot](screenshot02.png)
### Filtering Fonts w/ Category
![Third Screenshot](screenshot03.png)
chain_one_line_max = 100
comment_width = 100
condense_wildcard_suffixes = true
error_on_line_overflow = false
fn_call_width = 100
fn_single_line = true
format_strings = true
imports_indent = "Block"
match_pattern_separator_break_point = "Front"
max_width = 100
normalize_comments = true
reorder_imported_names = true
reorder_imports = true
reorder_imports_in_group = true
single_line_if_else_max_width = 100
struct_field_align_threshold = 30
where_style = "legacy"
wrap_comments = true
write_mode = "overwrite"
use gtk::*;
#[derive(Clone)]
pub struct Header {
pub container: HeaderBar,
pub font_size: SpinButton,
pub install: Button,
pub uninstall: Button
}
impl Header {
pub fn new() -> Header {
let container = HeaderBar::new();
container.set_show_close_button(true);
container.set_title("Font Finder");
let font_size = SpinButton::new(&Adjustment::new(2.0, 1.0, 50.0, 0.25, 0.0, 0.0), 0.1, 2);
let install = Button::new_with_label("Install");
install.set_visible(false);
let uninstall = Button::new_with_label("Uninstall");
uninstall.set_visible(false);
container.pack_start(&font_size);
container.pack_end(&install);
container.pack_end(&uninstall);
Header {
container,
font_size,
install,
uninstall
}
}
}
use fontfinder::fonts::Font;
use gtk::*;
use std::cell::RefCell;
use std::rc::Rc;
use webkit2gtk::*;
#[derive(Clone)]
pub struct Main {
pub container: Paned,
pub categories: ComboBoxText,
pub fonts_box: ListBox,
pub fonts: Rc<RefCell<Vec<FontRow>>>,
pub context: WebContext,
pub view: WebView,
pub sample_text: TextView,
pub sample_buffer: TextBuffer,
pub search: SearchEntry,
}
impl Main {
pub fn new(fonts_archive: &[Font], categories: &[String]) -> Main {
let container = Paned::new(Orientation::Horizontal);
// Generate a list box from the list of fonts in the archive.
let fonts_box = ListBox::new();
let mut fonts = Vec::with_capacity(fonts_archive.len());
for font in fonts_archive {
let row = FontRow::new(font.category.clone(), font.family.clone());
fonts_box.insert(&row.container, -1);
fonts.push(row);
}
// Allows the font list box to scroll
let scroller = ScrolledWindow::new(None, None);
scroller.set_min_content_width(200);
scroller.add(&fonts_box);
// The category menu for filtering based on category.
let menu = ComboBoxText::new();
menu.insert_text(0, "All");
for (id, category) in categories.iter().enumerate() {
menu.insert_text((id + 1) as i32, category.as_str());
}
menu.set_active(0);
// Search bar beneath the category menu for doing name-based filters.
let search = SearchEntry::new();
// Construct the left pane's box
let lbox = Box::new(Orientation::Vertical, 0);
lbox.pack_start(&menu, false, false, 0);
lbox.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0);
lbox.pack_start(&search, false, false, 0);
lbox.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0);
lbox.pack_start(&scroller, true, true, 0);
let context = WebContext::get_default().unwrap();
let view = WebView::new_with_context_and_user_content_manager(
&context,
&UserContentManager::new(),
);
let buffer = TextBuffer::new(None);
buffer.set_text(
"Lorem ipsum dolor sit amet, consectetur adipiscing \
elit, sed do eiusmod tempor incididunt ut labore \
et dolore magna aliqua.",
);
let sample_text = TextView::new_with_buffer(&buffer);
sample_text.set_wrap_mode(WrapMode::Word);
sample_text.set_left_margin(5);
sample_text.set_right_margin(5);
sample_text.set_top_margin(5);
sample_text.set_bottom_margin(5);
let rbox = Box::new(Orientation::Vertical, 0);
rbox.pack_start(&sample_text, false, false, 0);
rbox.pack_start(&Separator::new(Orientation::Horizontal), false, false, 0);
rbox.pack_start(&view, true, true, 0);
container.pack1(&lbox, false, false);
container.pack2(&rbox, true, true);
Main {
container,
categories: menu,
fonts_box,
fonts: Rc::new(RefCell::new(fonts)),
context,
view,
sample_text,
search,
sample_buffer: buffer
}
}
}
#[derive(Clone)]
pub struct FontRow {
pub container: ListBoxRow,
pub category: String,
pub family: String,
}
impl FontRow {
pub fn new(category: String, family: String) -> FontRow {
let container = ListBoxRow::new();
let label = Label::new("");
label.set_markup(&["<b>", family.as_str(), "</b>"].concat());
label.set_justify(Justification::Left);
container.add(&label);
FontRow {
container,
category,
family,
}
}
}
mod header;
mod main;
pub use self::header::Header;
pub use self::main::{FontRow, Main};
use fontfinder::fonts::FontsList;
use gtk::*;
#[derive(Clone)]
pub struct App {
pub window: Window,
pub header: Header,
pub main: Main,
}
impl App {
pub fn new(font_archive: &FontsList, categories: &[String]) -> App {
let window = Window::new(WindowType::Toplevel);
let header = Header::new();
let main = Main::new(&font_archive.items, categories);
window.set_titlebar(&header.container);
window.add(&main.container);
window.set_title("Font Finder");
window.set_default_size(600, 400);
window.set_wmclass("font-finder", "Font Finder");
window.connect_delete_event(move |_, _| {
main_quit();
Inhibit(false)
});
App {
window,
header,
main,
}
}
}
extern crate fontfinder;
extern crate gio;
extern crate gtk;
extern crate webkit2gtk;
mod gtk_ui;
use fontfinder::{dirs, fonts, html, FontError};
use gtk::*;
use gtk_ui::{App, FontRow};
use std::process;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use webkit2gtk::*;
fn main() {
// Initialize GTK before proceeding.
if gtk::init().is_err() {
eprintln!("failed to initialize GTK Application");
process::exit(1);
}
// Grab the information on Google's archive of free fonts.
// I'm wrapping it in Arc so it can be shared across multiple closures.
let fonts_archive = match fonts::obtain() {
Ok(fonts_archive) => Arc::new(fonts_archive),
Err(why) => {
eprintln!("failed to get font archive: {}", why);
process::exit(1);
}
};
// Collect a list of unique categories from that font list.
let categories = fonts_archive.get_categories();
// Contains the ID of the currently-selected row, to cut down on lookups.
let current_row_id = Arc::new(AtomicUsize::new(0));
// Initializes the complete structure of the GTK application.
// Contains all relevant widgets that we will manipulate.
let app = App::new(&fonts_archive, &categories);
// The following code will program the widgets in the UI. Each `clone()` will merely
// increment reference counters, and they exist to allow these widgets to be shared across
// multiple closures.
{
// Updates the UI when a row is selected.
let sample = app.main.sample_text.clone();
let preview = app.main.view.clone();
let rows = app.main.fonts.clone();
let list = app.main.fonts_box.clone();
let uninstall = app.header.uninstall.clone();
let install = app.header.install.clone();
let title = app.header.container.clone();
let size = app.header.font_size.clone();
let row_id = current_row_id.clone();
let fonts_archive = fonts_archive.clone();
list.connect_row_selected(move |_, row| if let Some(row) = row.clone() {
let id = row.get_index() as usize;
row_id.store(id, Ordering::SeqCst);
let font = &(*rows.borrow())[id];
title.set_title(font.family.as_str());
if let Some(sample_text) = get_text(&sample) {
html::generate(
&font.family,
size.get_value(),
&sample_text,
|html| preview.load_html(html, None),
);
}
match dirs::font_cache().ok_or(FontError::FontDirectory) {
Ok(path) => {
let font = fonts_archive.get_family(&font.family).unwrap();
let font_exists = font.files.iter().all(
|(variant, uri)| dirs::font_exists(&path, &font.family, &variant, &uri)
);
install.set_visible(!font_exists);
uninstall.set_visible(font_exists);
}
Err(why) => {
eprintln!("fontfinder: unable to get font cache: {}", why);
install.set_visible(false);
uninstall.set_visible(false);
}
}
});
}
{
// Updates the preview when the value of the font size spinner is changed.
let sample = app.main.sample_buffer.clone();
let preview = app.main.view.clone();
let rows = app.main.fonts.clone();
let size = app.header.font_size.clone();
let row_id = current_row_id.clone();
size.connect_property_value_notify(move |size| {
let font = &(*rows.borrow())[row_id.load(Ordering::SeqCst)];
if let Some(sample_text) = get_buffer(&sample) {
html::generate(
&font.family,
size.get_value(),
&sample_text,
|html| preview.load_html(html, None),
);
}
});
}
{
// Updates the preview when the sample text is updated.
let sample = app.main.sample_buffer.clone();
let preview = app.main.view.clone();
let rows = app.main.fonts.clone();
let size = app.header.font_size.clone();
let row_id = current_row_id.clone();
sample.connect_changed(move |sample| {
let font = &(*rows.borrow())[row_id.load(Ordering::SeqCst)];
if let Some(sample_text) = get_buffer(&sample) {
html::generate(
&font.family,
size.get_value(),
&sample_text,
|html| preview.load_html(html, None),
);
}
});
}
{
// Filters all fonts that don't match a selected category.
let category = app.main.categories.clone();
let rows = app.main.fonts.clone();
let search = app.main.search.clone();
category.connect_changed(move |category| if let Some(text) = category.get_active_text() {
filter_category(&text, get_search(&search), &rows.borrow());
});
}
{
// Filters fonts based on the search + category.
let category = app.main.categories.clone();
let rows = app.main.fonts.clone();
let search = app.main.search.clone();
search.connect_search_changed(
move |search| if let Some(text) = category.get_active_text() {
filter_category(&text, get_search(&search), &rows.borrow());
},
);
}
{
// Programs the install button
let install = app.header.install.clone();
let uninstall = app.header.uninstall.clone();
let row_id = current_row_id.clone();
let rows = app.main.fonts.clone();
let fonts_archive = fonts_archive.clone();
install.connect_clicked(move |install| {
let font = &(*rows.borrow())[row_id.load(Ordering::SeqCst)];
match fonts_archive.download(&font.family) {
Ok(_) => {
install.set_visible(false);
uninstall.set_visible(true);
}
Err(why) => eprintln!("fontfinder: unable to install font: {}", why),
}
});
}
{
// Programs the uninstall button
let install = app.header.install.clone();
let uninstall = app.header.uninstall.clone();
let row_id = current_row_id.clone();
let rows = app.main.fonts.clone();
let fonts_archive = fonts_archive.clone();
uninstall.connect_clicked(move |uninstall| {
let font = &(*rows.borrow())[row_id.load(Ordering::SeqCst)];
match fonts_archive.remove(&font.family) {
Ok(_) => {
uninstall.set_visible(false);
install.set_visible(true);
}
Err(why) => eprintln!("fontfinder: unable to remove font: {}", why),
}
});
}
app.window.show_all();
app.header.install.set_visible(false);
app.header.uninstall.set_visible(false);
gtk::main();
}
/// Filters visibility of associated font ListBoxRow's, according to a given category filter.
fn filter_category(category: &str, search: Option<String>, fonts: &[FontRow]) {
match search {
Some(ref search) if category == "All" => fonts.iter().for_each(
|font| font.container.set_visible(font.family.to_lowercase().contains(search)),
),
Some(ref search) => fonts.iter().for_each(|font| {
font.container.set_visible(
&font.category == category && font.family.to_lowercase().contains(search),
)
}),
None if category == "All" => fonts.iter().for_each(|font| font.container.set_visible(true)),
None => {
fonts.iter().for_each(|font| font.container.set_visible(&font.category == category))
}
}
}
/// Attempt to get the text from thhe given `TextView`'s `TextBuffer`.
fn get_text(view: &TextView) -> Option<String> { view.get_buffer().and_then(|x| get_buffer(&x)) }
fn get_buffer(buffer: &TextBuffer) -> Option<String> {
let start = buffer.get_start_iter();
let end = buffer.get_end_iter();
buffer.get_text(&start, &end, true)
}
fn get_search(search: &SearchEntry) -> Option<String> {
match search.get_text() {
Some(ref text) if text.is_empty() => None,
Some(ref text) => Some(text.to_lowercase()),
None => None,
}
}
use std::env;
use std::path::{Path, PathBuf};
pub fn font_cache() -> Option<PathBuf> {
env::home_dir().map(|path| path.join(".local/share/fonts/"))
}
pub fn font_exists(base: &Path, family: &str, variant: &str, uri: &str) -> bool {
get_font_path(base, family, variant, uri).exists()
}
pub fn get_font_path(base: &Path, family: &str, variant: &str, uri: &str) -> PathBuf {
let extension = uri.rfind('.').map_or("", |pos| &uri[pos..]);
base.join(&[family, "_", variant, extension].concat())
}
use {dirs, FontError};
use reqwest::{self, Client};
use std::collections::HashMap;
use std::fs::{self, OpenOptions};
use std::io;
const API_KEY: &str = "AIzaSyDpvpba_5RvJSvmXEJS7gZDezDaMlVTo4c";
lazy_static! {
static ref URL: String = {
format!("https://www.googleapis.com/webfonts/v1/webfonts?key={}", API_KEY)
};
}
#[derive(Deserialize)]
pub struct FontsList {
pub kind: String,
pub items: Vec<Font>,
}
impl FontsList {
/// Downloads/installs each variant of a given font family.
pub fn download(&self, family: &str) -> Result<(), FontError> {
// Initialize a client that will be re-used between requests.
let client = Client::new();
// Get the base directory of the local font directory
let path = dirs::font_cache().ok_or(FontError::FontDirectory)?;
// Find the given font in the font list and return it's reference.
let font = self.get_family(family).ok_or(FontError::FontNotFound)?;
// Download/install each variant of the given font family.
for (variant, uri) in &font.files {
// Create a variant of the path with this variant's filename.
let path = dirs::get_font_path(&path, family, &variant, &uri);
eprintln!("fontfinder: installing '{:?}'", path);
// Then create that file for writing.
let