├── .gitignore ├── ARCHITECTURE ├── CONTRIBUTING.rst ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.rst ├── arch └── delegation.txt ├── build.rs ├── circle.yml ├── contrib ├── README.rst └── scripts │ ├── back.lua │ ├── buffer.lua │ ├── buffernew.lua │ ├── buffernext.lua │ ├── bufferprev.lua │ ├── buku.lua │ ├── close.lua │ ├── copy.lua │ ├── endfind.lua │ ├── enforce-https.lua │ ├── example.lua │ ├── fail-banner.lua │ ├── find.lua │ ├── focus-bar.lua │ ├── focus-webview.lua │ ├── forward.lua │ ├── github.lua │ ├── go.lua │ ├── highlight-inputs.lua │ ├── highlight-links.lua │ ├── pinboard.lua │ ├── reload.lua │ ├── smart-search.lua │ ├── toggle-bar.lua │ ├── update-title.lua │ ├── user-content.lua │ └── windownew.lua ├── docs ├── Makefile ├── _static │ └── webkitten.png ├── _templates │ └── sidebar.html ├── conf.py ├── dev-guide │ ├── building.rst │ ├── contributing.rst │ ├── gui-binding.rst │ └── script-binding.rst ├── index.rst ├── requirements.txt └── user-guide │ ├── configuration-options.rst │ ├── scripting-with-lua.rst │ ├── webkitten-cocoa.rst │ └── webkitten-gtk.rst ├── macos ├── Cargo.lock ├── Cargo.toml └── src │ ├── appkit.rs │ ├── core_foundation.rs │ ├── core_graphics.rs │ ├── core_services.rs │ ├── foundation.rs │ ├── lib.rs │ └── webkit.rs ├── src ├── command.rs ├── config.rs ├── keybinding.rs ├── lib.rs ├── optparse.rs ├── script │ ├── lua.rs │ └── mod.rs └── ui.rs ├── webkitten-cocoa ├── Cargo.lock ├── Cargo.toml ├── app │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── webkitten@1024.png │ │ │ ├── webkitten@128.png │ │ │ ├── webkitten@16.png │ │ │ ├── webkitten@256-1.png │ │ │ ├── webkitten@256.png │ │ │ ├── webkitten@32-1.png │ │ │ ├── webkitten@32.png │ │ │ ├── webkitten@512-1.png │ │ │ ├── webkitten@512.png │ │ │ └── webkitten@64.png │ ├── Info.plist │ └── main.swift ├── src │ ├── main.rs │ ├── runtime.rs │ └── ui │ │ ├── application.rs │ │ ├── mod.rs │ │ └── window.rs └── webkitten.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Webkitten.xcscheme └── webkitten-gtk ├── Cargo.toml └── src ├── main.rs ├── ui ├── box_container.rs ├── mod.rs ├── text_field.rs ├── web_view.rs └── window.rs └── webkitgtk_sys └── mod.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /ARCHITECTURE: -------------------------------------------------------------------------------- 1 | 2 | 8 8 w w w 3 | Yb db dP .d88b 88b. 8.dP w w8ww w8ww .d88b 8d8b. 4 | YbdPYbdP 8.dP' 8 8 88b 8 8 8 8.dP' 8P Y8 5 | YP YP `Y88P 88P' 8 Yb 8 Y8P Y8P `Y88P 8 8 6 | 7 | ================================================== 8 | 9 | 10 | Webkitten is a command-driven web browser built on WebKit and inspired by 11 | luakit (https://mason-larobina.github.io/luakit) and Vim 12 | (http://www.vim.org). 13 | 14 | -- [CONTENTS] --- 15 | 16 | Goals...................................................................[GOALS] 17 | Layout.................................................................[LAYOUT] 18 | Components.........................................................[COMPONENTS] 19 | Default Commands.....................................................[COMMANDS] 20 | Common Configuration Options....................................[CONFIGURATION] 21 | 22 | -- [GOALS] -- 23 | 24 | * Human-readable configuration 25 | * SQLite bookmark storage 26 | * Scriptable interface 27 | * Private browsing modes 28 | * WebKit content blocking (https://webkit.org/blog/3476) 29 | * Command autocompletion 30 | * Customizable keybindings 31 | * User scripts and CSS support 32 | * Split pane support 33 | 34 | 35 | -- [LAYOUT] -- 36 | 37 | The webkitten window has three components: 38 | 39 | * Buffer pane: Main content pane, containing a swappable web view 40 | * Command Bar: Allows typing commands, displays ephemeral status info 41 | 42 | 43 | ┌────────────────────────────────────────────────────────────────┐ 44 | │ │ 45 | │ │ 46 | │ │ 47 | │ │ 48 | │ │ 49 | │ │ 50 | │ │ 51 | │ Buffers: Buffer[Web View] │ 52 | │ │ 53 | │ │ 54 | │ │ 55 | │ │ 56 | │ │ 57 | ├────────────────────────────────────────────────────────────────┤ 58 | │ Command Bar │ 59 | └────────────────────────────────────────────────────────────────┘ 60 | 61 | 62 | -- [COMPONENTS] -- 63 | 64 | Application: 65 | Runtime representation. Has windows and configuration 66 | 67 | Tasks: 68 | * Start/Stop new instance of app, provided a configuration file path 69 | * Reload configuration 70 | * Open a new window 71 | * Execute text as command 72 | 73 | Buffer: 74 | Web view container with an index 75 | 76 | Tasks: 77 | * Load/Reload URI 78 | 79 | Command: 80 | Alters app state. Resolved from text. 81 | 82 | Tasks: 83 | * Run 84 | * Provide completions 85 | 86 | Command Bar: 87 | Command text entry. Has history 88 | 89 | Tasks: 90 | * Set/Clear text 91 | * Set (colored) ephemeral text 92 | 93 | Widget Behavior: 94 | * Change contents on keypress up/down from history 95 | * Activate on return 96 | 97 | Command Parser: 98 | Creates a command. Has search paths 99 | 100 | Tasks: 101 | * Find and create command for text 102 | 103 | Configuration: 104 | All application preferences. Has load path 105 | 106 | Tasks: 107 | * Get value of string/int/array 108 | * Reload from path 109 | 110 | History: 111 | List of textual items 112 | 113 | Tasks: 114 | * Push/Pop/Clear 115 | * Change strategy (Save all/none/last n) 116 | * Get item at index 117 | 118 | Window: 119 | UI entry point. Has command bar, address bar, and buffers 120 | 121 | Tasks: 122 | * Add/Remove buffer 123 | * Switch buffer 124 | * Close 125 | 126 | Widget Behavior: 127 | * Focus command bar on command shortcut 128 | * Focus command bar in find mode on find shortcut 129 | 130 | Web View: 131 | Web renderer widget 132 | 133 | Tasks: 134 | * Go back/forward 135 | * Refresh 136 | * Open URI 137 | * Find (next instance of) text 138 | 139 | 140 | 141 | ┌──────────────────┐ executed by 142 | │ Application │─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 143 | └──────────────────┘ │ 144 | │ ┌──────────────────┐ 145 | ┌────────────────────┼───────────────────┐ │ Command │ 146 | │ ┼ │ └──────────────────┘ 147 | ┼ ┌┼┐ ┼ │ 148 | ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ │ 149 | │ Configuration │ │ Window │ │ Command Parser │─────────┘ 150 | └─────────────────┘ └──────────────────┘ └─────────────────┘ creates 151 | │ 152 | │───────────────────┐ 153 | ┼ ┼ 154 | ┌──────────────────┐ ┌─────────────────┐ 155 | │ Buffer Container │ │ Command Bar │ 156 | └──────────────────┘ └─────────────────┘ 157 | ┼ │ 158 | ┌┼┐ ┼ 159 | ┌──────────────────┐ ┌─────────────────┐ 160 | │ Buffer │ │ History │ 161 | └──────────────────┘ └─────────────────┘ 162 | │ 163 | ┼ 164 | ┌──────────────────┐ 165 | │ Web View │ 166 | └──────────────────┘ 167 | 168 | 169 | -- [COMMANDS] -- 170 | 171 | * `go URI`: Open URI 172 | * `forward [NUMBER]`: Move web view forward in history. NUMBER default is 1. 173 | * `back [NUMBER]`: Move web view back in history. NUMBER default is 1. 174 | * `openwindow [PRIVATE]`: Open a new window. PRIVATE default is config value. 175 | * `config edit`: Edit configuration file in $VISUAL, if set. 176 | * `config reload`: Reload configuration from file 177 | * `buffers`: List all buffers 178 | * `buffer NUMBER`: switch to buffer with NUMBER index 179 | * `bookmark save NAME`: Create bookmark to current page 180 | * `bookmark open NAME`: Open bookmark with name 181 | * `clearhistory`: Clear browsing history 182 | 183 | 184 | -- [CONFIGURATION] -- 185 | 186 | * alias.KEYS: Full name of command to execute when command KEYS is activated 187 | * command.NAME.enable: Boolean to disable a command named NAME 188 | * keybindings.CHORD: Key CHORD to trigger a command (optionally with arguments) 189 | * plugin.NAME.enable: Boolean to allow plugin such as Java or Silverlight 190 | * window.open-private: Boolean to open new windows in private mode 191 | * window.start-pages: URIs to open with each new window 192 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contribution Guide 2 | ================== 3 | 4 | The Webkitten project uses a pull request flow for accepting patches. To get 5 | started: 6 | 7 | 1. Fork the project 8 | 2. Create a new branch for each logically grouped set of changes. In general, 9 | patches with multiple unrelated changes will probably not be accepted. 10 | 3. Open a pull request with a description of the changes made and the rationale 11 | 12 | For bug fixes and adding new commands to the ``contrib/`` directory, no 13 | additional steps are necessary. However, for new features, best results can be 14 | achieved by first opening an issue to ensure its a direction which aligns with 15 | project goals. 16 | 17 | Building Webkitten 18 | ------------------ 19 | 20 | Webkitten depends on Rust 1.5+ with Cargo. Once installed, run ``make`` to 21 | download the Rust dependencies, build the library, and build default reference 22 | implementation. 23 | 24 | Testing 25 | ------- 26 | 27 | Use ``make test`` to run the library and default implementation tests. For 28 | other commands, see ``make help``. 29 | 30 | Development resources 31 | --------------------- 32 | 33 | Core 34 | **** 35 | 36 | Rust 37 | ~~~~ 38 | 39 | * The book (https://doc.rust-lang.org/stable/book) 40 | * Standard Library (http://doc.rust-lang.org/std) 41 | * libc (https://doc.rust-lang.org/stable/libc/index.html) 42 | * FFI guide (https://doc.rust-lang.org/book/ffi.html) 43 | * Cargo build script guide (http://doc.crates.io/build-script.html) 44 | * The Rustonomicon: Guide to Advanced/``unsafe`` Rust (https://doc.rust-lang.org/nightly/nomicon) 45 | 46 | Build System 47 | ~~~~~~~~~~~~ 48 | 49 | * Make reference (http://www.freebsd.org/doc/en/books/developers-handbook/tools-make.html) 50 | * pkg-config (https://www.freedesktop.org/wiki/Software/pkg-config) 51 | 52 | Documentation 53 | ~~~~~~~~~~~~~ 54 | 55 | The HTML documentation is compiled with Sphinx_ and reStructuredText_. Use ``pip 56 | install -r requirements.txt`` to install the dependencies and ``make doc`` to 57 | generate the files. 58 | 59 | Reference implementations 60 | ************************* 61 | 62 | GTK+ 63 | ~~~~ 64 | 65 | * Gtk+3 (https://developer.gnome.org/gtk3/stable) 66 | * GObject (https://developer.gnome.org/gobject/stable) 67 | * WebKit2Gtk+ (http://webkitgtk.org/reference/webkit2gtk/stable) 68 | * GtkSourceView (https://developer.gnome.org/gtksourceview/stable) 69 | * Gtk rust bindings (http://gtk-rs.org/docs) 70 | 71 | Cocoa 72 | ~~~~~ 73 | 74 | * AppKit Framework Reference 75 | (https://developer.apple.com/library/mac/documentation/Cocoa/Reference/ApplicationKit/ObjC_classic/index.html) 76 | 77 | WebKit 78 | ~~~~~~ 79 | 80 | * WebKit Wiki (http://trac.webkit.org/wiki) 81 | * WebKit Bugzilla (https://bugs.webkit.org) 82 | 83 | 84 | .. _Sphinx: http://www.sphinx-doc.org/en/stable 85 | .. _reStructuredText: http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html 86 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webkitten" 3 | version = "0.1.0" 4 | authors = ["Delisa Mason "] 5 | license = "BSD-2-Clause" 6 | description = "Core for running a configurable minimal browser" 7 | homepage = "https://webkitten.delisa.me" 8 | repository = "https://github.com/kattrali/webkitten" 9 | 10 | [dependencies] 11 | hlua = "0.1.8" 12 | toml = "0.1.30" 13 | getopts = "0.2.21" 14 | log = "0.4.8" 15 | url = "2.1.0" 16 | dirs = "2.0.2" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Delisa Mason 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 20 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Target installation directory 2 | DESTDIR := /usr/local 3 | # Subdirectory within $(DESTDIR) for installing the binaries 4 | DESTBIN := $(DESTDIR)/bin 5 | # Command to create a directory, default is BSD-style install 6 | INSTALLDIRCMD := install -d 7 | # Command to install file 8 | INSTALLCMD := install -C 9 | 10 | ifeq ($(shell uname),Darwin) 11 | PROJECT=webkitten-cocoa 12 | CARGO=cd $(PROJECT) && cargo 13 | CARGO_TEST=cargo 14 | else 15 | PROJECT=webkitten-gtk 16 | # Libraries required to build 17 | LIBS=webkit2gtk-4.0 gtk+-3.0 18 | # Linking flags for required libraries. The spaces are added for cargo compat. 19 | CFLAGS:= $(subst -L/,-L /,$(subst -l, -l ,$(shell pkg-config --libs $(LIBS)))) 20 | # Cargo build manager 21 | CARGO=cd $(PROJECT) && CFLAGS='$(CFLAGS)' cargo 22 | CARGO_TEST=CFLAGS='$(CFLAGS)' cargo 23 | endif 24 | 25 | SRC_FILES=$(shell ls src/*.rs $(PROJECT)/src/{**/,}*.rs) build.rs Cargo.toml 26 | DEV_FILE=$(PROJECT)/target/debug/$(PROJECT) 27 | PROD_FILE=$(PROJECT)/target/release/$(PROJECT) 28 | INSTALL_FILE=$(DESTBIN)/$(PROJECT) 29 | COCOA_APP=webkitten-cocoa/build/Release/Webkitten.app 30 | COCOA_SRC=webkitten-cocoa/app/main.swift 31 | 32 | all: build 33 | 34 | $(DEV_FILE): $(SRC_FILES) 35 | @$(CARGO) build 36 | 37 | $(PROD_FILE): $(SRC_FILES) 38 | @$(CARGO) build --release 39 | 40 | $(COCOA_APP): $(PROD_FILE) $(COCOA_SRC) 41 | @cd webkitten-cocoa && xcodebuild 42 | @echo Generated $(COCOA_APP) 43 | 44 | # Create the target directory for installing tool binaries if it does not 45 | # exist 46 | $(DESTBIN): 47 | @$(INSTALLDIRCMD) $(DESTBIN) 48 | 49 | .PHONY: build 50 | 51 | apidoc: ## Generate API documentation and open in the default browser 52 | @$(CARGO) doc --no-deps --open 53 | 54 | doc: ## Generate user/development documentation 55 | $(MAKE) -C docs html 56 | 57 | build: $(DEV_FILE) ## Build the webkitten binary 58 | 59 | cocoa: $(COCOA_APP) ## Build the Cocoa application wrapper 60 | 61 | cocoa-clean: ## Clean the Cocoa wrapper build artifact 62 | @rm -r $(COCOA_APP) 63 | @cd webkitten-cocoa && xcodebuild clean 64 | 65 | release: $(PROD_FILE) ## Build the webkitten binary in release mode 66 | 67 | install: $(PROD_FILE) ## Install webkitten into $DESTDIR/bin 68 | @$(INSTALLDIRCMD) $(DESTDIR)/bin 69 | @$(INSTALLCMD) $(PROD_FILE) $(INSTALL_FILE) 70 | 71 | uninstall: ## Remove webkitten from $DESTDIR/bin 72 | @rm $(INSTALL_FILE) 73 | 74 | clean: cocoa-clean ## Clean the build environment 75 | @$(CARGO) clean 76 | 77 | run: ## Run webkitten in development mode 78 | @RUST_LOG='info' $(CARGO) run 79 | 80 | test: ## Run the webkitten test suite 81 | @$(CARGO_TEST) test 82 | @$(CARGO) test 83 | 84 | help: ## Show help text 85 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 86 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | webkitten 2 | ========= 3 | 4 | Webkitten is a command-driven web browser toolkit inspired by luakit_ and Vim_. 5 | 6 | Webkitten allows you to: 7 | 8 | - Browse the web (nearly) pointing device-free 9 | - Run custom scripts for browser interaction on demand or triggered by events 10 | - Edit human-readable configuration files 11 | - Assign keybindings to your custom scripts 12 | - Alter web pages with custom CSS and JavaScript 13 | - Create custom browsing modes based on the sites you visit 14 | - Customize your own content blocking 15 | 16 | Running a reference implementation 17 | ---------------------------------- 18 | 19 | In addition to the tooling, Webkitten includes two reference implementations of 20 | the browser interface: 21 | 22 | - webkitten-cocoa_: A Cocoa WebKit implementation of Webkitten with Lua 23 | scripting 24 | - webkitten-gtk_: [WIP] A WebKit2 GTK+3 implementation of Webkitten with Lua 25 | scripting 26 | 27 | Use ``make run`` to run the default implementation for your platform, and see 28 | the `User Guide`_ and the contrib_ directory for commands to kick start your 29 | configuration. Use ``make install`` to install the binary into your ``PATH``. 30 | 31 | My personal configuration files are here_ for reference. 32 | 33 | Building your own browser 34 | ------------------------- 35 | 36 | Using the webkitten toolkit requires implementing the `ui` module and starting 37 | the application with an implementation of `ui::ApplicationUI`: 38 | 39 | .. code-block:: rust 40 | 41 | // Create runtime configuration 42 | let run_config = RunConfiguration { 43 | path: path_to_config_toml, 44 | start_pages: vec!["https://example.com"] 45 | }; 46 | 47 | // Create engine 48 | let engine = Engine::new(run_config); 49 | 50 | // Create UI 51 | let mut ui = MyCustomUI::new(engine); 52 | 53 | // Go go go 54 | ui.run(); 55 | 56 | Then the UI should notify the ``EventHandler`` when events occur, such as 57 | pressing the Return key in the command bar or web content failing to load. 58 | Provided this contract is met, the scripting engine can automate interactions 59 | with the UI, making it easy to customize. 60 | 61 | While named "webkitten", new UI bindings do not necessarily need to be 62 | WebKit-based, though the bindings were designed with WebKit in mind. 63 | 64 | Development 65 | ----------- 66 | 67 | Webkitten is largely written in Rust and uses Cargo_ for dependency management. 68 | Questions, suggestions, and patches welcome - see the `Contribution Guide`_ for 69 | more information. 70 | 71 | Building 72 | ~~~~~~~~ 73 | 74 | To build, run `make`. To run the reference implementations, use `make run`. 75 | 76 | For all other commands, try `make help`. 77 | 78 | .. _luakit: https://mason-larobina.github.io/luakit 79 | .. _Vim: https://www.vim.org 80 | .. _webkitten-gtk: webkitten-gtk 81 | .. _webkitten-cocoa: webkitten-cocoa 82 | .. _`User Guide`: https://delisa.me/webkitten 83 | .. _contrib: contrib/scripts 84 | .. _Cargo: https://docs.crates.io 85 | .. _`Contribution Guide`: CONTRIBUTING.rst 86 | .. _here: https://github.com/kattrali/dotfiles/tree/master/.config/webkitten 87 | -------------------------------------------------------------------------------- /arch/delegation.txt: -------------------------------------------------------------------------------- 1 | ┌────────────────────────────────────────────────────────┐ 2 | │ CommandBar │ 3 | └────────────────────────────────────────────────────────┘ 4 | ┌────────────────────────────────────────────────────────┐ 5 | │ fn resolve_command() -> Result │ 6 | └────────────────────────────────────────────────────────┘ 7 | ┌─────────────────────────┐ ┌────────────────────────────┐ 8 | │ fn run(exe: Executable) │ │ fn display_error(err:) │ 9 | └─────────────────────────┘ └────────────────────────────┘ 10 | 11 | 12 | ┌────────────────────────────────────────────────────────┐ 13 | │ Application │ 14 | └────────────────────────────────────────────────────────┘ 15 | ┌─────────────────────────┐ ┌────────────────────────────┐ 16 | │ fn open_window() │ │ fn close_focused_window() │ 17 | └─────────────────────────┘ └────────────────────────────┘ 18 | 19 | 20 | ┌────────────────────────────────────────────────────────┐ 21 | │ Window │ 22 | └────────────────────────────────────────────────────────┘ 23 | ┌─────────────────────────┐ ┌────────────────────────────┐ 24 | │ fn close() │ │ fn split(direction:) │ 25 | └─────────────────────────┘ └────────────────────────────┘ -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | fn main() { 4 | match env::var("CFLAGS") { 5 | Ok(cflags) => println!("cargo:rustc-flags={}", cflags), 6 | _ => {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | cache_directories: 3 | - ~/.cargo 4 | machine: 5 | pre: 6 | - curl -sS https://static.rust-lang.org/rustup.sh | sudo sh 7 | 8 | test: 9 | override: 10 | # https://discuss.circleci.com/t/cargo-build-fails-on-fresh-install/1102/3 11 | - rm ~/.gitconfig && eval `ssh-agent` && ssh-add /home/ubuntu/.ssh/id_circleci_github && cargo test 12 | -------------------------------------------------------------------------------- /contrib/README.rst: -------------------------------------------------------------------------------- 1 | Contrib 2 | ======= 3 | 4 | Example extensions and scripts for webkitten apps 5 | -------------------------------------------------------------------------------- /contrib/scripts/back.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Navigates back in the focused web view" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | go_back(windex, focused_webview_index(windex)) 8 | return true 9 | end 10 | -------------------------------------------------------------------------------- /contrib/scripts/buffer.lua: -------------------------------------------------------------------------------- 1 | function run() 2 | windex = focused_window_index() 3 | if #arguments > 0 then 4 | index = tonumber(arguments[1]) 5 | if index > webview_count(windex) then 6 | return false 7 | end 8 | focus_webview(windex, tonumber(arguments[1])) 9 | end 10 | return true 11 | end 12 | 13 | -- Display titles for buffers 14 | function complete_command() 15 | local titles = {} 16 | local query = table.concat(arguments," ") 17 | local windex = focused_window_index() 18 | local total = webview_count(windex) 19 | for i = 0, total - 1 do 20 | title = "" .. i .. " : " .. webview_title(windex, i):gsub(","," ") 21 | if string.sub(title, 1, #query) == query then 22 | titles[#titles + 1] = title 23 | end 24 | end 25 | return table.concat(titles,",") 26 | end 27 | -------------------------------------------------------------------------------- /contrib/scripts/buffernew.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Opens a new buffer with a URL or configured start page" 3 | end 4 | 5 | function run() 6 | local target = "" 7 | local windex = focused_window_index() 8 | if #arguments > 0 then 9 | target = arguments[1] 10 | else 11 | target = lookup_string(config_file_path, "window.start-page") 12 | end 13 | 14 | if windex ~= NOT_FOUND then 15 | open_webview(windex, target) 16 | else 17 | open_window(target) 18 | end 19 | 20 | return true 21 | end 22 | -------------------------------------------------------------------------------- /contrib/scripts/buffernext.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Cycles to the next buffer in the window" 3 | end 4 | 5 | function run() 6 | windex = focused_window_index() 7 | target = focused_webview_index(windex) + 1 8 | if target >= webview_count(windex) then 9 | target = 0 10 | end 11 | focus_webview(windex, target) 12 | return true 13 | end 14 | -------------------------------------------------------------------------------- /contrib/scripts/bufferprev.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Cycles to the previous buffer in the window" 3 | end 4 | 5 | function run() 6 | windex = focused_window_index() 7 | target = focused_webview_index(windex) - 1 8 | if target < 0 then 9 | target = webview_count(windex) - 1 10 | end 11 | focus_webview(windex, target) 12 | return true 13 | end 14 | -------------------------------------------------------------------------------- /contrib/scripts/buku.lua: -------------------------------------------------------------------------------- 1 | -- Bookmark management using buku (https://github.com/jarun/Buku) 2 | -- and jq (https://stedolan.github.io/jq/) 3 | function description() 4 | return "Save and open bookmarks" 5 | end 6 | 7 | function run() 8 | if #arguments > 0 then 9 | command = arguments[1] 10 | table.remove(arguments, 1) 11 | window_index = focused_window_index() 12 | webview_index = focused_webview_index(window_index) 13 | if command == "open" then 14 | return open_bookmark(window_index, webview_index) 15 | elseif command == "save" then 16 | return save_bookmark(window_index, webview_index) 17 | else 18 | log_info(string.format("Unknown command provided: %s", command)) 19 | end 20 | end 21 | return false 22 | end 23 | 24 | -- Open the first bookmark matching arguments 25 | function open_bookmark(window_index, webview_index) 26 | query = table.concat(arguments, " ") 27 | command = string.format("buku -j --noprompt -s '%s' | jq 'map(.uri)[0]'", query) 28 | handle = io.popen(command) 29 | uri = handle:read("*a"):gsub("\"", ""):gsub("%s+$", "") 30 | if #uri > 1 then 31 | load_uri(window_index, webview_index, uri) 32 | return true 33 | end 34 | return false 35 | end 36 | 37 | -- Save a bookmark using the webview title and URI and arguments as tags 38 | function save_bookmark(window_index, webview_index) 39 | title = webview_title(window_index, webview_index) 40 | uri = webview_uri(window_index, webview_index) 41 | if #uri > 0 then 42 | tags = table.concat(arguments, ",") 43 | command = string.format("buku --noprompt -a %s --title '%s' --tag '%s'", uri, title, tags) 44 | log_info(string.format("Running command: %s", command)) 45 | return os.execute(command) == 0 46 | end 47 | return false 48 | end 49 | 50 | -- Provide completions from the titles of bookmarks 51 | function complete_command() 52 | if #arguments > 0 then 53 | if arguments[1] == "open" then 54 | query = string.format("buku -j -s '%s' | jq 'map(.title)[]'", prefix:gsub("buku open ", "")) 55 | handle = io.popen(query) 56 | text = handle:read("*a") 57 | return text:gsub("\"", ""):gsub("\n", ",") 58 | else 59 | return "" 60 | end 61 | elseif #arguments == 1 then 62 | for i, item in ipairs({"open","save"}) do 63 | if #item >= #arguments[1] then 64 | if string.sub(item, 1, #arguments[1]) == arguments[1] then 65 | return item 66 | end 67 | end 68 | end 69 | end 70 | return "open,save" 71 | end 72 | -------------------------------------------------------------------------------- /contrib/scripts/close.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Closes the current webview or window" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | if webview_count(windex) > 1 then 8 | close_webview(windex, focused_webview_index(windex)) 9 | else 10 | close_window(windex) 11 | end 12 | return true 13 | end 14 | 15 | -------------------------------------------------------------------------------- /contrib/scripts/copy.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Copy the URL" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | if windex == NOT_FOUND then 8 | return false 9 | end 10 | copy(webview_uri(windex, focused_webview_index(windex))) 11 | return true 12 | end 13 | -------------------------------------------------------------------------------- /contrib/scripts/endfind.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Hide results from 'find'" 3 | end 4 | 5 | 6 | function run() 7 | windex = focused_window_index() 8 | hide_find(windex, focused_webview_index(windex)) 9 | return true 10 | end 11 | -------------------------------------------------------------------------------- /contrib/scripts/enforce-https.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Coerces HTTP URLs to HTTPS" 3 | end 4 | 5 | -- Redirects from HTTP to HTTPS, except where excluded in the configuration 6 | -- option `enforce-https.ignored-hosts` 7 | function on_request_uri() 8 | target = requested_uri 9 | if string.find(target, ".local") or string.find(target, "localhost") then 10 | return 11 | end 12 | ignored_hosts = lookup_strings(config_file_path, "enforce-https.ignored-hosts") 13 | host = string.gmatch(string.match(requested_uri, "://(.*)"), "[^/]+")() 14 | for _, ignored_host in ipairs(ignored_hosts) do 15 | if ignored_host == host then 16 | return 17 | end 18 | end 19 | if string.find(target, "http://") then 20 | target = target:gsub("http://", "https://") 21 | log_info("Redirecting to HTTPS") 22 | load_uri(window_index, webview_index, target) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /contrib/scripts/example.lua: -------------------------------------------------------------------------------- 1 | -- This script's purpose (required) 2 | function description() 3 | return "An example script documenting all hooks" 4 | end 5 | 6 | -- Run default function. The current scope should include `arguments` as a 7 | -- function which returns relevant options. Should return a boolean indicating 8 | -- success or failure 9 | function run() 10 | log_info("Running the example script") 11 | return true 12 | end 13 | 14 | -- Provide an array of completions given a prefix. The current scope should 15 | -- include a `prefix` function which returns the relevant state. Should return 16 | -- a comma-delimited list of items as a string 17 | function complete_command() 18 | return "" 19 | end 20 | 21 | -- Invoked when a URI will be loaded in a webview. The current scope includes a 22 | -- `webview_index` and `window_index` indicating which view is active, as well 23 | -- as `requested_uri` indicating what URI was requested. Should return a string 24 | -- which is the URI which should be loaded. 25 | -- 26 | -- This hook is only invoked if the command name is included in the 27 | -- configuration option `commands.on-request-uri` 28 | function on_request_uri() 29 | requested_uri 30 | end 31 | 32 | -- Invoked when a URI is loaded in a webview. The current scope includes a 33 | -- `webview_index` and `window_index` indicating which view is active. 34 | -- 35 | -- This hook is only invoked if the command name is included in the 36 | -- configuration option `commands.on-load-uri` 37 | function on_load_uri() 38 | end 39 | -------------------------------------------------------------------------------- /contrib/scripts/fail-banner.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Shows a banner with an error message when a page fails to load" 3 | end 4 | 5 | function on_fail_uri() 6 | run_javascript(window_index, webview_index, string.format([[ 7 | var element = document.createElement("div"); 8 | element.style.position = "fixed"; 9 | element.style.top = 0; 10 | element.style.left = 0; 11 | element.style.zIndex = 9999; 12 | element.style.background = "red"; 13 | element.style.width = "100%%"; 14 | element.style.color = "white"; 15 | element.onclick = function () { this.parentElement.removeChild(this) }; 16 | var message = document.createTextNode("Failed to load %s : %s Click to dismiss."); 17 | element.appendChild(message); 18 | document.body.appendChild(element); 19 | ]], requested_uri, clean_message(error_message))) 20 | end 21 | 22 | function clean_message(message) 23 | return message: 24 | gsub("&","&"): 25 | gsub("<","<"): 26 | gsub(">", ">"): 27 | gsub('"', '"'): 28 | gsub("'", '''): 29 | gsub("/", '/') 30 | end 31 | -------------------------------------------------------------------------------- /contrib/scripts/find.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Find text in the current webview" 3 | end 4 | 5 | 6 | function run() 7 | if #arguments > 0 then 8 | windex = focused_window_index() 9 | query = table.concat(arguments, " ") 10 | find(windex, focused_webview_index(windex), query) 11 | end 12 | return false -- continue to display command 13 | end 14 | -------------------------------------------------------------------------------- /contrib/scripts/focus-bar.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Focuses the command bar" 3 | end 4 | 5 | function run() 6 | focus_commandbar_in_window(focused_window_index()) 7 | return true 8 | end 9 | -------------------------------------------------------------------------------- /contrib/scripts/focus-webview.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Focuses the webview" 3 | end 4 | 5 | function run() 6 | focus_webview_in_window(focused_window_index()) 7 | return true 8 | end 9 | 10 | -------------------------------------------------------------------------------- /contrib/scripts/forward.lua: -------------------------------------------------------------------------------- 1 | function run() 2 | windex = focused_window_index() 3 | go_forward(windex, focused_webview_index(windex)) 4 | return true 5 | end 6 | 7 | -------------------------------------------------------------------------------- /contrib/scripts/github.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Opens a repo, performs a search on GitHub, or opens a common page" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | local webview_index = focused_webview_index(windex) 8 | if #arguments > 0 then 9 | if arguments[1] == "status" then 10 | load_uri(windex, webview_index, "https://status.github.com") 11 | elseif arguments[1] == "gist" then 12 | load_uri(windex, webview_index, "https://gist.github.com") 13 | elseif arguments[1] == "help" then 14 | load_uri(windex, webview_index, "https://help.github.com") 15 | elseif arguments[1] == "api" then 16 | load_uri(windex, webview_index, "https://developer.github.com/v3/") 17 | elseif arguments[1] == "dev" then 18 | load_uri(windex, webview_index, "https://developer.github.com") 19 | else 20 | local owner, repo = string.match(arguments[1], "([%w-]+)/([%w-]+)") 21 | if owner ~= nil then 22 | target = string.format("https://github.com/%s/%s", owner, repo) 23 | load_uri(windex, webview_index, target) 24 | else 25 | query = url_encode(table.concat(arguments, " ")) 26 | target = table.concat({"https://github.com/search?q=", query}, "") 27 | load_uri(windex, webview_index, target) 28 | end 29 | end 30 | else 31 | load_uri(windex, webview_index, "https://github.com") 32 | end 33 | return true 34 | end 35 | 36 | function complete_command() 37 | local subcommands = {"api","dev","gist", "help", "status"} 38 | local query = table.concat(arguments," ") 39 | if #query == 0 then 40 | return table.concat(subcommands,",") 41 | end 42 | for i, cmd in pairs(subcommands) do 43 | if string.sub(cmd, 1, #query) == query then 44 | return cmd 45 | end 46 | end 47 | return "" 48 | end 49 | 50 | function url_encode(str) 51 | if (str) then 52 | str = string.gsub (str, "\n", "\r\n") 53 | str = string.gsub (str, "([^%w %-%_%.%~])", 54 | function (c) return string.format ("%%%02X", string.byte(c)) end) 55 | str = string.gsub (str, " ", "+") 56 | end 57 | return str 58 | end 59 | -------------------------------------------------------------------------------- /contrib/scripts/go.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Open a web page" 3 | end 4 | 5 | function run() 6 | if #arguments > 0 then 7 | windex = focused_window_index() 8 | target = arguments[1] 9 | load_uri(windex, focused_webview_index(windex), target) 10 | return true 11 | end 12 | log_debug("No URL specified") 13 | return false 14 | end 15 | -------------------------------------------------------------------------------- /contrib/scripts/highlight-inputs.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Highlights and focuses inputs/buttons without a mouse" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | local webdex = focused_webview_index(windex) 8 | run_javascript(windex, webdex, [[ 9 | var inputs = document.querySelectorAll("input,button"), 10 | target = [], timer = null, 11 | ESC_CODE = 27, ENTER_CODE = 10, ZERO_CODE = 48, NINE_CODE = 57; 12 | for (var i = 0; i < inputs.length; i++) { 13 | var span = document.createElement("span"); 14 | span.className = "hinput"; 15 | span.innerText = "" + i; 16 | span.setAttribute("style","color: #888; vertical-align: bottom; border: 1px solid #ccc; border-radius: 2px; background: DEECFA; font-family: sans-serif; font-size: 11px"); 17 | inputs[i].parentElement.insertBefore(span, inputs[i]); 18 | } 19 | document.onkeydown = function(event) { 20 | var clearFunc = function() { 21 | target = []; 22 | var spans = document.querySelectorAll("span.hinput"); 23 | for (var i = 0; i < spans.length; i++) { 24 | spans[i].remove(); 25 | } 26 | if (timer) { 27 | window.clearTimeout(timer); 28 | } 29 | document.onkeydown = null; 30 | }; 31 | var focusFunc = function() { 32 | if (document.activeElement && document.activeElement.tagName != "BODY") { 33 | return; // don't perform action from input areas 34 | } 35 | var selected = inputs[parseInt(target.join(""))]; 36 | if (selected) { 37 | selected.focus(); 38 | } 39 | clearFunc(); 40 | }; 41 | if (event.keyCode == ENTER_CODE) { 42 | focusFunc(); 43 | } else if (event.keyCode >= ZERO_CODE && event.keyCode <= NINE_CODE) { 44 | target.push(String.fromCharCode(event.keyCode)); 45 | if (timer) { 46 | window.clearTimeout(timer); 47 | } 48 | timer = window.setTimeout(focusFunc, 800); 49 | } else if (event.keyCode == ESC_CODE) { 50 | clearFunc(); 51 | } 52 | }; 53 | ]]) 54 | focus_webview_in_window(windex) 55 | return true 56 | end 57 | -------------------------------------------------------------------------------- /contrib/scripts/highlight-links.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Highlights and opens links without a mouse" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | local webdex = focused_webview_index(windex) 8 | run_javascript(windex, webdex, [[ 9 | var links = document.getElementsByTagName("a"), 10 | target = [], timer = null, 11 | ESC_CODE = 27, ENTER_CODE = 10, ZERO_CODE = 48, NINE_CODE = 57; 12 | for (var i = 0; i < links.length; i++) { 13 | var span = document.createElement("span"); 14 | span.className = "hlink"; 15 | span.innerText = "" + i; 16 | span.setAttribute("style","color: #888; vertical-align: top; border: 1px solid #ccc; border-radius: 2px; background: yellow; font-family: sans-serif; font-size: 11px"); 17 | links[i].insertAdjacentElement("afterbegin", span); 18 | } 19 | document.onkeydown = function(event) { 20 | var clickFunc = function() { 21 | if (document.activeElement && document.activeElement.tagName != "BODY") { 22 | return; // don't perform action from input areas 23 | } 24 | var selected = links[parseInt(target.join(""))]; 25 | if (selected) { 26 | selected.click(); 27 | } 28 | target = []; 29 | if (timer) { 30 | window.clearTimeout(timer); 31 | } 32 | }; 33 | if (event.keyCode == ENTER_CODE) { 34 | clickFunc(); 35 | } else if (event.keyCode >= ZERO_CODE && event.keyCode <= NINE_CODE) { 36 | target.push(String.fromCharCode(event.keyCode)); 37 | if (timer) { 38 | window.clearTimeout(timer); 39 | } 40 | timer = window.setTimeout(clickFunc, 800); 41 | } else if (event.keyCode == ESC_CODE) { 42 | target = []; 43 | var spans = document.querySelectorAll("span.hlink"); 44 | for (var i = 0; i < spans.length; i++) { 45 | spans[i].remove(); 46 | } 47 | if (timer) { 48 | window.clearTimeout(timer); 49 | } 50 | document.onkeydown = null; 51 | } 52 | }; 53 | ]]) 54 | focus_webview_in_window(windex) 55 | return true 56 | end 57 | -------------------------------------------------------------------------------- /contrib/scripts/pinboard.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Pinboard bookmarking" 3 | end 4 | 5 | function run() 6 | window_index = focused_window_index() 7 | webview_index = focused_webview_index(window_index) 8 | if #arguments > 0 then 9 | command = arguments[1] 10 | table.remove(arguments, 1) 11 | if command == "same" then -- save to bookmarks in same page 12 | script = "javascript:if(document.getSelection){s=document.getSelection();}else{s='';};document.location='https://pinboard.in/add?next=same&url='+encodeURIComponent(location.href)+'&description='+encodeURIComponent(s)+'&title='+encodeURIComponent(document.title)" 13 | run_javascript(window_index, webview_index, script) 14 | elseif command == "rl" then -- read later 15 | script = "javascript:q=location.href;p=document.title;void(t=open('https://pinboard.in/add?later=yes&noui=yes&jump=close&url='+encodeURIComponent(q)+'&title='+encodeURIComponent(p),'Pinboard','toolbar=no,width=100,height=100'));t.blur();" 16 | run_javascript(window_index, webview_index, script) 17 | elseif command == "oldest" then -- view oldest unread item 18 | load_uri(window_index, webview_index, "https://pinboard.in/oldest/") 19 | elseif command == "random" then -- view random unread item 20 | load_uri(window_index, webview_index, "https://pinboard.in/random/?type=unread") 21 | else 22 | return false 23 | end 24 | else 25 | load_uri(window_index, webview_index, "https://pinboard.in/") 26 | end 27 | return true 28 | end 29 | 30 | function complete_command() 31 | if #arguments == 0 then 32 | return "oldest,rl,same,random" 33 | elseif #arguments == 1 then 34 | for i, item in ipairs({"oldest","rl","same","random"}) do 35 | if #item >= #arguments[1] then 36 | if string.sub(item, 1, #arguments[1]) == arguments[1] then 37 | return item 38 | end 39 | end 40 | end 41 | end 42 | return "" 43 | end 44 | -------------------------------------------------------------------------------- /contrib/scripts/reload.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Reloads the current view" 3 | end 4 | 5 | function run() 6 | window_index = focused_window_index() 7 | webview_index = focused_webview_index(window_index) 8 | disable_blockers = false 9 | if #arguments == 0 then 10 | reload_webview(window_index, webview_index, false) 11 | return true 12 | elseif #arguments == 1 and (arguments[1] == "f" or arguments[1] == "force") then 13 | reload_webview(window_index, webview_index, true) 14 | return true 15 | end 16 | log_info("Invalid arguments passed to 'reload'") 17 | return false 18 | end 19 | -------------------------------------------------------------------------------- /contrib/scripts/smart-search.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Smart defaults command for opening a website or doing a search" 3 | end 4 | 5 | -- Opens anything that looks like a URL directly, or falls back to a web search 6 | -- using the configuration option `general.search-engine-url` to construct the 7 | -- query. The search engine option defaults to DuckDuckGo. 8 | function run() 9 | local query = table.concat(arguments, " ") 10 | if query:match("^([^?][%w%p]+%.[%w%p]+)$") then 11 | open_uri(query) 12 | else 13 | local engine = lookup_string(config_file_path, "general.search-engine-url") 14 | if #engine == 0 then 15 | engine = "https://duckduckgo.com" 16 | end 17 | open_uri(table.concat({engine, "?q=", url_encode(query)}, "")) 18 | end 19 | return true 20 | end 21 | 22 | function open_uri(target) 23 | local windex = focused_window_index() 24 | if windex ~= NOT_FOUND then 25 | load_uri(windex, focused_webview_index(windex), target) 26 | else 27 | open_window(target) 28 | end 29 | end 30 | 31 | function url_encode(str) 32 | if (str) then 33 | str = string.gsub (str, "\n", "\r\n") 34 | str = string.gsub (str, "([^%w %-%_%.%~])", 35 | function (c) return string.format ("%%%02X", string.byte(c)) end) 36 | str = string.gsub (str, " ", "+") 37 | end 38 | return str 39 | end 40 | -------------------------------------------------------------------------------- /contrib/scripts/toggle-bar.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Toggle the visibility of the command bar" 3 | end 4 | 5 | function run() 6 | local windex = focused_window_index() 7 | local visible = command_field_visible(windex) == false 8 | set_command_field_visible(windex, visible) 9 | if visible then 10 | focus_commandbar_in_window(windex) 11 | else 12 | focus_webview_in_window(windex) 13 | end 14 | return true 15 | end 16 | -------------------------------------------------------------------------------- /contrib/scripts/update-title.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Update window title based on buffer content" 3 | end 4 | 5 | function on_focus() 6 | update_title(window_index, webview_index) 7 | end 8 | 9 | function on_fail_uri() 10 | update_title(window_index, webview_index) 11 | end 12 | 13 | function on_request_uri() 14 | set_window_title(window_index, default_title(window_index) .. "Loading...") 15 | end 16 | 17 | function on_load_uri() 18 | update_title(window_index, webview_index) 19 | end 20 | 21 | function update_title(window_index, webview_index) 22 | title = webview_title(window_index, webview_index) 23 | if #title > 0 then 24 | set_window_title(window_index, default_title(window_index) .. title) 25 | end 26 | end 27 | 28 | function default_title(window_index) 29 | index = focused_webview_index(window_index) 30 | return "(" .. index .. ") " 31 | end 32 | -------------------------------------------------------------------------------- /contrib/scripts/user-content.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Loads custom CSS and JS" 3 | end 4 | 5 | -- Loads a CSS or JS file into the current document 6 | function run() 7 | if #arguments > 0 then 8 | local file_name = arguments[1] 9 | log_info("loading " .. file_name) 10 | window_index = focused_window_index() 11 | webview_index = focused_webview_index(window_index) 12 | if string.ends_with(file_name, ".css") then 13 | return load_css(file_name, window_index, webview_index) 14 | elseif string.ends_with(file_name, ".js") then 15 | return load_js(file_name, window_index, webview_index) 16 | end 17 | end 18 | return false 19 | end 20 | 21 | -- Loads all CSS and JS files from the configuration option 22 | -- `user-content.default-paths` into every page, as well as the files from 23 | -- `user-content.site-paths` based on the domain name of the page. 24 | function on_load_uri() 25 | load_default_files() 26 | load_site_files() 27 | end 28 | 29 | function load_site_files() 30 | host = string.gmatch(string.match(requested_uri, "://(.*)"), "[^/]+")() 31 | base_paths = lookup_strings(config_file_path, "user-content.site-paths") 32 | for _, base_path in ipairs(base_paths) do 33 | load_css(string.format("%s/%s.css", base_path, host), window_index, webview_index) 34 | load_js(string.format("%s/%s.js", base_path, host), window_index, webview_index) 35 | end 36 | end 37 | 38 | function load_default_files() 39 | default_paths = lookup_strings(config_file_path, "user-content.default-paths") 40 | for _, base_path in ipairs(default_paths) do 41 | for _, file_name in ipairs(list_files(base_path)) do 42 | if string.ends_with(file_name, ".css") then 43 | load_css(string.format("%s/%s", base_path, file_name), window_index, webview_index) 44 | elseif string.ends_with(file_name, ".js") then 45 | load_js(string.format("%s/%s.js", base_path, file_name), window_index, webview_index) 46 | end 47 | end 48 | end 49 | end 50 | 51 | function load_css(path, window_index, webview_index) 52 | css = load_file(path) 53 | if css then 54 | log_info(string.format("Loading CSS: %s", path)) 55 | add_styles(window_index, webview_index, css) 56 | return true 57 | end 58 | return false 59 | end 60 | 61 | function load_js(path, window_index, webview_index) 62 | js = load_file(path) 63 | if js then 64 | log_info(string.format("Loading JS: %s", path)) 65 | run_javascript(window_index, webview_index, js) 66 | return true 67 | end 68 | return false 69 | end 70 | 71 | function load_file(file_path) 72 | local file = io.open(file_path) 73 | if not file then return nil end 74 | local content = file:read("*a") 75 | file:close() 76 | return content 77 | end 78 | 79 | function list_files(directory) 80 | local index, files = 0, {} 81 | local list_handle = io.popen('ls "' .. directory .. '"') 82 | for filename in list_handle:lines() do 83 | index = index + 1 84 | files[index] = filename 85 | end 86 | list_handle:close() 87 | return files 88 | end 89 | 90 | function string.ends_with(text, ending) 91 | return ending == '' or string.sub(text, -string.len(ending)) == ending 92 | end 93 | -------------------------------------------------------------------------------- /contrib/scripts/windownew.lua: -------------------------------------------------------------------------------- 1 | function description() 2 | return "Opens a new window with a URL or configured start page" 3 | end 4 | 5 | function run() 6 | local target = "" 7 | if #arguments > 0 then 8 | target = arguments[1] 9 | else 10 | target = lookup_string(config_file_path, "window.start-page") 11 | end 12 | open_window(target) 13 | return true 14 | end 15 | 16 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = ../target 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Webkitten.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Webkitten.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Webkitten" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Webkitten" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/_static/webkitten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/docs/_static/webkitten.png -------------------------------------------------------------------------------- /docs/_templates/sidebar.html: -------------------------------------------------------------------------------- 1 |

Webkitten

2 | 3 |

4 | 8 |

9 | A command-driven web browser toolkit inspired by luakit and Vim. 10 | 11 | 23 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Webkitten documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Mar 19 18:13:11 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | import sys 17 | import os 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | source_parsers = {} 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # source_suffix = ['.rst', '.md'] 42 | source_suffix = '.rst' 43 | 44 | # The encoding of source files. 45 | #source_encoding = 'utf-8-sig' 46 | 47 | # The master toctree document. 48 | master_doc = 'index' 49 | 50 | # General information about the project. 51 | project = 'Webkitten' 52 | copyright = '2016, Delisa Mason' 53 | author = 'Delisa Mason' 54 | 55 | # The version info for the project you're documenting, acts as replacement for 56 | # |version| and |release|, also used in various other places throughout the 57 | # built documents. 58 | # 59 | # The short X.Y version. 60 | version = '1.0.0' 61 | # The full version, including alpha/beta/rc tags. 62 | release = '1.0.0' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # There are two options for replacing |today|: either, you set today to some 72 | # non-false value, then it is used: 73 | #today = '' 74 | # Else, today_fmt is used as the format for a strftime call. 75 | #today_fmt = '%B %d, %Y' 76 | 77 | # List of patterns, relative to source directory, that match files and 78 | # directories to ignore when looking for source files. 79 | exclude_patterns = ['_build'] 80 | 81 | # The reST default role (used for this markup: `text`) to use for all 82 | # documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | # If true, keep warnings as "system message" paragraphs in the built documents. 103 | #keep_warnings = False 104 | 105 | # If true, `todo` and `todoList` produce output, else they produce nothing. 106 | todo_include_todos = False 107 | 108 | 109 | # -- Options for HTML output ---------------------------------------------- 110 | 111 | # The theme to use for HTML and HTML Help pages. See the documentation for 112 | # a list of builtin themes. 113 | html_theme = 'alabaster' 114 | 115 | # Theme options are theme-specific and customize the look and feel of a theme 116 | # further. For a list of options available for each theme, see the 117 | # documentation. 118 | #html_theme_options = {} 119 | 120 | # Add any paths that contain custom themes here, relative to this directory. 121 | #html_theme_path = [] 122 | 123 | # The name for this set of Sphinx documents. If None, it defaults to 124 | # " v documentation". 125 | #html_title = None 126 | 127 | # A shorter title for the navigation bar. Default is the same as html_title. 128 | #html_short_title = None 129 | 130 | # The name of an image file (relative to this directory) to place at the top 131 | # of the sidebar. 132 | html_logo = "_static/webkitten.png" 133 | 134 | # The name of an image file (relative to this directory) to use as a favicon of 135 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 136 | # pixels large. 137 | #html_favicon = None 138 | 139 | # Add any paths that contain custom static files (such as style sheets) here, 140 | # relative to this directory. They are copied after the builtin static files, 141 | # so a file named "default.css" will overwrite the builtin "default.css". 142 | html_static_path = ['_static'] 143 | 144 | # Add any extra paths that contain custom files (such as robots.txt or 145 | # .htaccess) here, relative to this directory. These files are copied 146 | # directly to the root of the documentation. 147 | #html_extra_path = [] 148 | 149 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 150 | # using the given strftime format. 151 | #html_last_updated_fmt = '%b %d, %Y' 152 | 153 | # If true, SmartyPants will be used to convert quotes and dashes to 154 | # typographically correct entities. 155 | #html_use_smartypants = True 156 | 157 | # Custom sidebar templates, maps document names to template names. 158 | html_sidebars = { 159 | 'index': ['sidebar.html', 'searchbox.html'], 160 | '**': ['sidebar.html', 'localtoc.html', 'relations.html', 'searchbox.html'], 161 | } 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'Webkittendoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'Webkitten.tex', 'Webkitten Documentation', 231 | 'Delisa Mason', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'webkitten', 'Webkitten Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'Webkitten', 'Webkitten Documentation', 275 | author, 'Webkitten', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | -------------------------------------------------------------------------------- /docs/dev-guide/building.rst: -------------------------------------------------------------------------------- 1 | Building Webkitten 2 | ================== 3 | -------------------------------------------------------------------------------- /docs/dev-guide/contributing.rst: -------------------------------------------------------------------------------- 1 | Contribution guide 2 | ================== 3 | 4 | Core components 5 | --------------- 6 | 7 | Custom commands 8 | --------------- 9 | 10 | GUI or script binding implementations 11 | ------------------------------------- 12 | -------------------------------------------------------------------------------- /docs/dev-guide/gui-binding.rst: -------------------------------------------------------------------------------- 1 | Creating a GUI binding 2 | ====================== 3 | -------------------------------------------------------------------------------- /docs/dev-guide/script-binding.rst: -------------------------------------------------------------------------------- 1 | Creating a script binding 2 | ========================= 3 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Webkitten browser toolkit 2 | ========================= 3 | 4 | Webkitten is a command-driven web browser toolkit inspired by luakit_ and Vim_. 5 | 6 | Webkitten allows you to: 7 | 8 | - Browse the web (nearly) pointing device-free 9 | - Run custom scripts for browser interaction on demand or triggered by events 10 | - Edit human-readable configuration files 11 | - Assign keybindings to your custom scripts 12 | - Alter web pages with custom CSS and JavaScript 13 | - Create custom browsing modes based on the sites you visit 14 | - Customize your own content blocking 15 | 16 | In addition to the tooling, Webkitten includes two reference implementation of 17 | a the browser interface: 18 | 19 | - webkitten-cocoa_: A Cocoa WebKit implementation of Webkitten with Lua 20 | scripting 21 | - webkitten-gtk_: [WIP] A WebKit2 GTK+3 implementation of Webkitten with Lua 22 | scripting 23 | 24 | User Guide 25 | ---------- 26 | 27 | .. toctree:: 28 | :maxdepth: 2 29 | 30 | user-guide/webkitten-cocoa 31 | user-guide/webkitten-gtk 32 | user-guide/configuration-options 33 | user-guide/scripting-with-lua 34 | 35 | Developer Guide 36 | --------------- 37 | 38 | .. toctree:: 39 | :maxdepth: 2 40 | 41 | dev-guide/building 42 | dev-guide/gui-binding 43 | dev-guide/script-binding 44 | dev-guide/contributing 45 | 46 | .. _luakit: https://mason-larobina.github.io/luakit 47 | .. _Vim: https://www.vim.org 48 | .. _webkitten-gtk: user-guide/webkitten-gtk.html 49 | .. _webkitten-cocoa: user-guide/webkitten-cocoa.html 50 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.3.6 2 | -------------------------------------------------------------------------------- /docs/user-guide/configuration-options.rst: -------------------------------------------------------------------------------- 1 | Configuration options 2 | ===================== 3 | 4 | Webkitten supports several configuration options relating to content filtering, 5 | browser capability, appearance, and command execution. These options are the 6 | basis of any Webkitten-based browser. 7 | 8 | Commands may define additional options, but these are supported by default. 9 | 10 | General 11 | ------- 12 | 13 | Base defaults for window appearance and new web view buffer behavior. 14 | 15 | .. glossary:: 16 | 17 | general.allow-javascript 18 | If ``true``, JavaScript will be permitted to run within new web view 19 | buffers. If unset, this value defaults to ``true``. 20 | 21 | general.allow-plugins 22 | If ``true``, browser plugins such as Silverlight and Flash will be 23 | permitted to load. If unset, this value defaults to ``false``. 24 | 25 | general.bar-font 26 | A pair of values, ``size`` and ``family``, which represent the font to be 27 | used in the command bar. If unset, font preference is left to the GUI 28 | binding implementation. 29 | 30 | general.config-dir 31 | The configuration directory which can be substituted with ``CONFIG_DIR`` 32 | within other options requiring file paths 33 | 34 | general.content-filter 35 | A path to a file containing content filtering rules to be applied by 36 | default. If unset, no content filtering is applied. 37 | 38 | general.private-browsing 39 | If ``true``, new web view buffers are opened in private browsing mode by 40 | default. No browsing history or content can be persisted from these 41 | sessions. If unset, this value defaults to ``false``. 42 | 43 | general.skip-content-filter 44 | If ``true``, the content filter file is not applied to new web view 45 | buffers. 46 | 47 | general.start-page 48 | A file or HTTP url indicating what content should be loaded in new web 49 | view buffers. 50 | 51 | Commands 52 | -------- 53 | 54 | Options regarding command loading, event triggers, and shortcuts. 55 | 56 | .. glossary:: 57 | 58 | commands.aliases."[ALIAS]" 59 | A command name to be invoked when the command bar text matches ``[ALIAS]`` 60 | 61 | commands.default 62 | The command invoked when no command files are found matching the first 63 | word 64 | 65 | commands.disabled 66 | Disabled commands by name, which are skipped when resolving commands 67 | 68 | commands.keybindings."[COMMAND]" 69 | A key chord representation which should invoke ``[COMMAND]`` when pressed. 70 | Each chord is represented by a combination of ``super``/``command``, 71 | ``ctrl``, ``alt``/``option``, and ``shift``, combined with a single 72 | character and separated by spaces. I.e., ``cmd shift n``. 73 | 74 | commands.on-fail-uri 75 | An array of command names to invoke when a resource fails to load 76 | 77 | commands.on-load-uri 78 | An array of command names to invoke when a resource loads 79 | 80 | commands.on-request-uri 81 | An array of command names to invoke when a resource is requested 82 | 83 | commands.on-text-change."[CHAR]" 84 | A command name to invoke as text changes in the command bar while the 85 | first character is ``[CHAR]``. 86 | 87 | commands.search-paths 88 | An array of string paths used to search for command files 89 | 90 | Site-specific options 91 | --------------------- 92 | 93 | General configuration options regarding web view behavior can be overridden 94 | when opening a buffer by following a link to a host and defining site-specific 95 | configuration options. These options are: 96 | 97 | .. glossary:: 98 | 99 | sites."[HOST]".general.allow-javascript 100 | If ``true``, any new buffers opened while linking to ``[HOST]`` will 101 | enable JavaScript to run. 102 | 103 | sites."[HOST]".general.allow-plugins 104 | If ``true``, any new buffers opened while linking to ``[HOST]`` will 105 | enable browser plugins such as Silverlight and Flash. 106 | 107 | sites."[HOST]".general.private-browsing 108 | If ``true``, any new buffers opened while linking to ``[HOST]`` will 109 | enable private browsing. 110 | 111 | sites."[HOST]".general.skip-content-filter 112 | If ``true``, any new buffers opened while linking to ``[HOST]`` will 113 | not load the content filter file. 114 | -------------------------------------------------------------------------------- /docs/user-guide/scripting-with-lua.rst: -------------------------------------------------------------------------------- 1 | Scripting with Lua 2 | ================== 3 | 4 | Webkitten includes a Lua 5.2 scripting engine for executing scripts based on 5 | input to the command bar and other event triggers. 6 | 7 | When a command is run in Webkitten, it is matched to a file found within a 8 | directory specified by the configuration option ``commands.search-paths``. For 9 | example, if ``reading-mode on`` is entered in the command bar, Webkitten checks 10 | the command search paths for a file named ``reading-mode.lua``, and if a match 11 | is found, the file contents are evaluated and the ``run()`` method is called, 12 | provided the script is not in some way malformed. 13 | 14 | Each command runs in a new Lua runtime, so there is no interaction between 15 | different commands. 16 | 17 | Event triggers 18 | -------------- 19 | 20 | The available entry points for running a command within Webkitten. Implement 21 | any of the following methods to run when an event is triggered. Note that some 22 | events are only triggered if a configuration prerequisite is met and that the 23 | ``description()`` method is required. 24 | 25 | .. glossary:: 26 | 27 | ``complete_commands()`` 28 | Provides completions to command arguments. The scope of the function 29 | includes a ``prefix`` variable which returns the full text the user has 30 | entered, as well as a table of each individual argument as 31 | ``arguments``. Returns a comma-delimited list of items as a string, or 32 | an empty string if no results were found. 33 | 34 | .. code-block:: lua 35 | 36 | function complete_command() 37 | return "open,close,save" 38 | end 39 | 40 | ``description()`` 41 | A summary of the script's purpose, intended for GUI bindings to display 42 | in autocompletion and help text. Returns a string. (required) 43 | 44 | .. code-block:: lua 45 | 46 | function description() 47 | return "An example script documenting all hooks" 48 | end 49 | 50 | ``on_fail_uri()`` 51 | Invoked when a URI fails to load. The current scope includes a 52 | ``webview_index`` and ``window_index`` indicating which view is active, 53 | as well as ``requested_uri`` indicating what URI was requested. 54 | 55 | This hook is only invoked if the command name is included in the 56 | configuration option ``commands.on-fail-uri``. 57 | 58 | .. code-block:: lua 59 | 60 | function on_fail_uri() 61 | log_debug("Failed to load " .. requested_uri) 62 | end 63 | 64 | ``on_load_uri()`` 65 | Invoked when a URI is loaded in a webview. The current scope includes a 66 | ``webview_index`` and ``window_index`` indicating which view is active, 67 | as well as ``requested_uri`` indicating what URI was requested. 68 | 69 | This hook is only invoked if the command name is included in the 70 | configuration option ``commands.on-load-uri``. 71 | 72 | .. code-block:: lua 73 | 74 | function on_load_uri() 75 | local uri = webview_uri(window_index, webview_index) 76 | log_debug(string.format("Just loaded %s", uri)) 77 | end 78 | 79 | ``on_request_uri()`` 80 | Invoked when a URI will be loaded in a webview. The current scope 81 | includes a ``webview_index`` and ``window_index`` indicating which view 82 | is active, as well as ``requested_uri`` indicating what URI was 83 | requested. 84 | 85 | This hook is only invoked if the command name is included in the 86 | configuration option ``commands.on-request-uri``. 87 | 88 | .. code-block:: lua 89 | 90 | function on_request_uri() 91 | log_debug("Requested to load " .. requested_uri) 92 | end 93 | 94 | ``run()`` 95 | The default hook, invoked when the user presses Return in the command 96 | bar. The scope of the function includes an ``arguments`` variable, which 97 | is a table of the space-delimited arguments which were passed with the 98 | function. Returns a boolean indicating whether to clear the bar text. 99 | 100 | .. code-block:: lua 101 | 102 | function run() 103 | log_info("Running the example script") 104 | return true 105 | end 106 | 107 | Constants 108 | --------- 109 | 110 | The Lua scripting engine provides a few constant values to make it easier to 111 | validate values or check configuration options. 112 | 113 | .. glossary:: 114 | 115 | ``CONFIG_FILE_PATH`` 116 | The path to the configuration file being used by Webkitten. It can be 117 | used as a convenience to do configuration value lookup. 118 | 119 | ``NOT_FOUND`` 120 | This is a possible value returned from the ``focused_window_index`` or 121 | ``focused_webview_index`` methods respectively, if there is no window or 122 | webview to correspond to the provided values. For example, if there are 123 | no windows open, ``focused_window_index()`` returns ``NOT_FOUND``. 124 | 125 | Provided methods 126 | ---------------- 127 | 128 | In addition to the `Lua standard libraries`_, the following global methods are 129 | provided for use within event triggers. 130 | 131 | .. glossary:: 132 | 133 | ``add_styles(window_index, webview_index, css)`` 134 | Inject CSS into a webview 135 | 136 | .. code-block:: lua 137 | 138 | function run() 139 | local window_index = focused_window_index() 140 | local webview_index = focused_webview_index(window_index) 141 | add_styles(window_index, webview_index, [[ 142 | body { background-color: red; } 143 | ]]) 144 | end 145 | 146 | ``close_webview(window_index, webview_index)`` 147 | Close a webview at a given index 148 | 149 | ``close_window(window_index)`` 150 | Close a window with a given index 151 | 152 | ``command_field_text(window_index)`` 153 | The text in the command bar of a window at a given index 154 | 155 | ``command_field_visible(window_index)`` 156 | Return ``true`` if in the command bar of a window at a given index is 157 | visible 158 | 159 | ``copy(string)`` 160 | Copy text to the native clipboard 161 | 162 | ``find(int, int, string)`` 163 | Find and highlight text in a webview 164 | 165 | ``focus_commandbar_in_window(window_index)`` 166 | Assign keyboard focus to the command field area of the window at a given 167 | index 168 | 169 | ``focus_webview(window_index, webview_index)`` 170 | Show a webview at a given index and assign keyboard focus to it 171 | 172 | ``focus_webview_in_window(window_index)`` 173 | Assign keyboard focus to the webview area of the window at a given index 174 | 175 | ``focused_webview_index(int)`` 176 | Returns the index of the focused webview in a window at a given index or 177 | ``NOT_FOUND`` 178 | 179 | ``focused_window_index()`` 180 | Returns the index of the focused window or ``NOT_FOUND`` 181 | 182 | ``go_back(window_index, webview_index)`` 183 | Returns to the previously loaded resource (if any) in a webview at a 184 | given index 185 | 186 | ``go_forward(window_index, webview_index)`` 187 | Loads the next resource (if any) in a webview at a given index 188 | 189 | ``hide_find(window_index, webview_index)`` 190 | Hide any GUI elements or highlighting relating to finding text onscreen 191 | 192 | ``hide_window(window_index)`` 193 | Hide a window at a given index 194 | 195 | ``load_uri(window_index, webview_index, string)`` 196 | Load a resource from a URI in a webview at a given index 197 | 198 | ``log_debug(message)`` 199 | Write text to the application log with a severity level of debug 200 | 201 | ``log_info(message)`` 202 | Write text to the application log with a severity level of info 203 | 204 | ``lookup_bool(string)`` 205 | Gets a bool value from the user's configuration file using the argument 206 | as a key 207 | 208 | ``lookup_string(config_path, key)`` 209 | Gets a string value from the user's configuration file using the 210 | argument as a key 211 | 212 | ``lookup_strings(config_path, key)`` 213 | Gets a table of strings from the user's configuration file using the 214 | argument as a key 215 | 216 | ``open_webview(window_index, uri)`` 217 | Open a new webview in a window at a given index and load the URI 218 | 219 | ``open_window(uri)`` 220 | Open a new window and load the URI 221 | 222 | ``reload_webview(int, int, bool)`` 223 | Reload a webview, optionally skipping content filters 224 | 225 | ``resize_window(window_index, width, height)`` 226 | Resize a window to the specified width and height 227 | 228 | ``run_javascript(window_index, webview_index, script)`` 229 | Run JavaScript source code in the webview at a given index 230 | 231 | ``set_command_field_text(window_index, text)`` 232 | Change the command field text in a window at a given index 233 | 234 | ``set_command_field_visible(window_index, is_visible)`` 235 | Change the command field visibility in a window at a given index 236 | 237 | ``set_window_title(window_index, title)`` 238 | Change the title in a window at a given index 239 | 240 | ``show_window(window_index)`` 241 | Show a previously hidden window by index 242 | 243 | ``webview_count(window_index)`` 244 | Returns the number of webviews contained in a window at a given index or 245 | zero if a window does not exist for that index 246 | 247 | ``webview_title(window_index, webview_index)`` 248 | The title of the web content in a webview at a given index 249 | 250 | ``webview_uri(window_index, webview_index)`` 251 | The URI of the loaded resource in a webview at a given index 252 | 253 | ``window_count()`` 254 | The number of windows currently open 255 | 256 | ``window_title(window_index)`` 257 | The title of the window at a given index or empty string if the index 258 | does not correspond to a window 259 | 260 | .. _`Lua standard libraries`: https://www.lua.org/manual/5.2/manual.html#6 261 | -------------------------------------------------------------------------------- /docs/user-guide/webkitten-cocoa.rst: -------------------------------------------------------------------------------- 1 | Webkitten for Cocoa 2 | =================== 3 | 4 | A native Cocoa implementation of the webkitten interface, integrated with the 5 | Lua scripting engine. 6 | 7 | Installation 8 | ------------ 9 | 10 | Webkitten depends on: 11 | 12 | - OS X 10.10+ 13 | 14 | From source 15 | ~~~~~~~~~~~ 16 | 17 | Development dependencies: 18 | 19 | - make 20 | - (BSD) install, or be comfortable editing the Makefile environment 21 | - Rust 1.5+, with Cargo (http://doc.crates.io) 22 | 23 | Once the dependencies are satisfied, run ``make cocoa`` from the root of the 24 | repository to generate the ``Webkitten.app`` bundle. 25 | 26 | Homebrew 27 | ~~~~~~~~ 28 | 29 | Install ``webkitten-cocoa`` from tap using 30 | 31 | .. code-block:: bash 32 | 33 | brew cask install kattrali/formulae/webkitten-cocoa 34 | 35 | Options 36 | ------- 37 | 38 | The webkitten-cocoa binary can be used to open a URI (or several) directly and 39 | specify a custom configuration path. However, the default mode for running the 40 | app is through an app bundle, obviating these options. 41 | 42 | .. code-block:: text 43 | 44 | Usage: webkitten-cocoa [options] [FILE] [FILE ...] 45 | 46 | Options: 47 | -c, --config PATH Use this configuration path 48 | -h, --help Print this help text 49 | 50 | Customization 51 | ------------- 52 | 53 | At this point, you can run Webkitten.app, but it is a blank slate without 54 | customized configuration or registered commands. Its a matter of personal 55 | preference how you want webkitten to work for you and what commands and keyboard 56 | shortcuts (among other things) should be available. 57 | 58 | 1. View the `configuration options reference`_ and customize your configuration 59 | as needed. The default configuration file location is 60 | ``$HOME/.config/webkitten/config.toml``. 61 | 2. Select commands from webkitten/contrib_ which may be useful to you and 62 | install them into your `command search path`_. 63 | 3. Write any other commands you need to make your browser perfect. 64 | 65 | Next steps 66 | ---------- 67 | 68 | .. toctree:: 69 | :maxdepth: 2 70 | 71 | configuration-options 72 | scripting-with-lua 73 | 74 | .. _command search path: configuration-options.html#command-search-path 75 | .. _configuration options reference: configuration-options.html 76 | .. _contrib: https://github.com/kattrali/webkitten/tree/master/contrib 77 | -------------------------------------------------------------------------------- /docs/user-guide/webkitten-gtk.rst: -------------------------------------------------------------------------------- 1 | Webkitten for GTK+ 2 | ================== 3 | 4 | A GTK+3 implementation of the webkitten interface, integrated with the Lua 5 | scripting engine. 6 | 7 | Installation 8 | ------------ 9 | 10 | Webkitten depends on: 11 | 12 | - `GTK+3`_ 13 | - `WebKit2GTK+`_ 14 | - GtkSourceView_ 15 | 16 | From source 17 | ~~~~~~~~~~~ 18 | 19 | Development dependencies: 20 | 21 | - make 22 | - pkg-config 23 | - (BSD) install, or be comfortable editing the Makefile environment 24 | - Rust 1.5+, with Cargo (http://doc.crates.io) 25 | 26 | Once the dependencies are satisfied, run ``make gtk`` from the root of the 27 | repository to generate the ``webkitten-gtk`` binary. 28 | 29 | Homebrew 30 | ~~~~~~~~ 31 | 32 | Install ``webkitten-gtk`` from tap using 33 | 34 | .. code-block:: bash 35 | 36 | brew install kattrali/formulae/webkitten-gtk 37 | 38 | 39 | Options 40 | ------- 41 | 42 | The webkitten-cocoa binary can be used to open a URI (or several) directly and 43 | specify a custom configuration path. 44 | 45 | .. code-block:: text 46 | 47 | Usage: webkitten-gtk [options] [FILE] 48 | 49 | Options: 50 | -c, --config PATH Use this configuration path 51 | -h, --help Print this help text 52 | 53 | Customization 54 | ------------- 55 | 56 | At this point, you can run ``webkitten-gtk``, but it is a blank slate without 57 | customized configuration or registered commands. Its a matter of personal 58 | preference how you want webkitten to work for you and what commands and keyboard 59 | shortcuts (among other things) should be available. 60 | 61 | 1. View the `configuration options reference`_ and customize your configuration 62 | as needed 63 | 2. Select commands from webkitten/contrib_ which may be useful to you and 64 | install them into your `command search path`_. 65 | 3. Write any other commands you need to make your browser perfect. 66 | 67 | Next steps 68 | ---------- 69 | 70 | .. toctree:: 71 | :maxdepth: 2 72 | 73 | configuration-options 74 | scripting-with-lua 75 | 76 | .. _command search path: configuration-options.html#command-search-path 77 | .. _configuration options reference: configuration-options.html 78 | .. _contrib: https://github.com/kattrali/webkitten/tree/master/contrib 79 | .. _`GTK+3`: http://www.gtk.org 80 | .. _`WebKit2GTK+`: https://webkitgtk.org 81 | .. _GtkSourceView: https://developer.gnome.org/gtksourceview/unstable/pt01.html 82 | -------------------------------------------------------------------------------- /macos/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "block" 5 | version = "0.1.6" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | 8 | [[package]] 9 | name = "libc" 10 | version = "0.2.66" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [[package]] 14 | name = "macos" 15 | version = "0.1.0" 16 | dependencies = [ 17 | "block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", 18 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", 19 | "objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", 20 | ] 21 | 22 | [[package]] 23 | name = "malloc_buf" 24 | version = "0.0.6" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | dependencies = [ 27 | "libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)", 28 | ] 29 | 30 | [[package]] 31 | name = "objc" 32 | version = "0.2.7" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | dependencies = [ 35 | "malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)", 36 | ] 37 | 38 | [metadata] 39 | "checksum block 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" 40 | "checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558" 41 | "checksum malloc_buf 0.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" 42 | "checksum objc 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" 43 | -------------------------------------------------------------------------------- /macos/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "macos" 3 | version = "0.1.0" 4 | authors = ["Delisa Mason "] 5 | 6 | [dependencies] 7 | block = "0.1.6" 8 | libc = "0.2.66" 9 | objc = "0.2.7" 10 | -------------------------------------------------------------------------------- /macos/src/core_foundation.rs: -------------------------------------------------------------------------------- 1 | use libc; 2 | 3 | 4 | #[repr(C)] 5 | pub struct __CFString(libc::c_void); 6 | pub type CFStringRef = *const __CFString; 7 | 8 | type CFAllocatorRef = *const libc::c_void; 9 | type CFIndex = libc::c_long; 10 | type CFStringEncoding = u32; 11 | 12 | #[link(name = "CoreFoundation", kind = "framework")] 13 | extern { 14 | static kCFAllocatorDefault: CFAllocatorRef; 15 | static kCFAllocatorNull: CFAllocatorRef; 16 | fn CFStringCreateWithBytes(alloc: CFAllocatorRef, 17 | bytes: *const u8, 18 | numBytes: CFIndex, 19 | encoding: CFStringEncoding, 20 | isExternalRepresentation: u8, 21 | contentsDeallocator: CFAllocatorRef) 22 | -> CFStringRef; 23 | } 24 | 25 | pub fn create_string_ref(content: &str) -> CFStringRef { 26 | static STRING_ENCODING_UTF8: CFStringEncoding = 0x08000100; 27 | unsafe { 28 | CFStringCreateWithBytes(kCFAllocatorDefault, 29 | content.as_ptr(), 30 | content.len() as CFIndex, 31 | STRING_ENCODING_UTF8, 32 | false as u8, 33 | kCFAllocatorNull) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /macos/src/core_graphics.rs: -------------------------------------------------------------------------------- 1 | use libc; 2 | 3 | 4 | #[cfg(target_pointer_width = "64")] 5 | pub type CGFloat = libc::c_double; 6 | #[cfg(not(target_pointer_width = "64"))] 7 | pub type CGFloat = libc::c_float; 8 | 9 | #[repr(C)] 10 | pub struct CGPoint { 11 | pub x: CGFloat, 12 | pub y: CGFloat, 13 | } 14 | 15 | #[repr(C)] 16 | pub struct CGRect { 17 | pub origin: CGPoint, 18 | pub size: CGSize, 19 | } 20 | 21 | #[repr(C)] 22 | pub struct CGSize { 23 | pub width: CGFloat, 24 | pub height: CGFloat, 25 | } 26 | 27 | impl CGRect { 28 | 29 | pub fn zero() -> CGRect { 30 | CGRect { 31 | origin: CGPoint { x: 0., y: 0. }, 32 | size: CGSize { width: 0., height: 0. } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /macos/src/core_services.rs: -------------------------------------------------------------------------------- 1 | use core_foundation::{CFStringRef,create_string_ref}; 2 | 3 | 4 | #[link(name = "CoreServices", kind = "framework")] 5 | extern { 6 | fn LSSetDefaultHandlerForURLScheme(scheme: CFStringRef, bundle_id:CFStringRef); 7 | } 8 | 9 | pub fn register_default_scheme_handler(scheme: &str, bundle_id: &str) { 10 | let scheme = create_string_ref(scheme); 11 | let bundle_id = create_string_ref(bundle_id); 12 | unsafe { LSSetDefaultHandlerForURLScheme(scheme, bundle_id); } 13 | } 14 | -------------------------------------------------------------------------------- /macos/src/foundation.rs: -------------------------------------------------------------------------------- 1 | use std::{str,slice}; 2 | use std::cmp::PartialEq; 3 | 4 | use objc::{Encode,Encoding}; 5 | use objc::runtime::{Object,Class,YES,BOOL}; 6 | use libc; 7 | 8 | use super::{Id,ObjCClass}; 9 | use core_graphics::{CGRect,CGSize,CGPoint}; 10 | 11 | 12 | #[link(name = "Foundation", kind = "framework")] 13 | extern {} 14 | 15 | #[cfg(target_pointer_width = "32")] 16 | pub type NSInteger = libc::c_int; 17 | #[cfg(target_pointer_width = "32")] 18 | pub type NSUInteger = libc::c_uint; 19 | 20 | #[cfg(target_pointer_width = "64")] 21 | pub type NSInteger = libc::c_long; 22 | #[cfg(target_pointer_width = "64")] 23 | pub type NSUInteger = libc::c_ulong; 24 | 25 | pub type NSRect = CGRect; 26 | pub type NSSize = CGSize; 27 | pub type NSPoint = CGPoint; 28 | 29 | const UTF8_ENCODING: NSUInteger = 4; 30 | 31 | impl_objc_class!(NSAppleEventDescriptor); 32 | impl_objc_class!(NSAppleEventManager); 33 | impl_objc_class!(NSArray); 34 | impl_objc_class!(NSAutoreleasePool); 35 | impl_objc_class!(NSBundle); 36 | impl_objc_class!(NSDictionary); 37 | impl_objc_class!(NSError); 38 | impl_objc_class!(NSMutableArray); 39 | impl_objc_class!(NSNotification); 40 | impl_objc_class!(NSNumber); 41 | impl_objc_class!(NSProcessInfo); 42 | impl_objc_class!(NSString); 43 | impl_objc_class!(NSURL); 44 | impl_objc_class!(NSURLRequest); 45 | 46 | impl NSAppleEventDescriptor { 47 | 48 | pub fn url_param_value(&self) -> Option { 49 | const URL_KEYWORD: u32 = 757935405; 50 | NSString::from_ptr(unsafe { 51 | let descriptor: Id = msg_send![self.ptr, paramDescriptorForKeyword:URL_KEYWORD]; 52 | msg_send![descriptor, stringValue] 53 | }) 54 | } 55 | } 56 | 57 | impl NSAppleEventManager { 58 | 59 | pub fn shared_manager() -> Self { 60 | NSAppleEventManager { 61 | ptr: unsafe { 62 | msg_send![class!(NSAppleEventManager), sharedAppleEventManager] 63 | } 64 | } 65 | } 66 | 67 | pub fn set_get_url_event_handler(&self, handler: &T) { 68 | const INTERNET_EVENT_CLASS: u32 = 1196773964; 69 | const GET_URL_EVENT_ID: u32 = 1196773964; 70 | unsafe { 71 | msg_send![self.ptr, setEventHandler:handler.ptr() 72 | andSelector:sel!(handleGetURLEvent:withReplyEvent:) 73 | forEventClass:INTERNET_EVENT_CLASS 74 | andEventID:GET_URL_EVENT_ID] 75 | } 76 | } 77 | } 78 | 79 | impl NSArray { 80 | 81 | /// Create an array from a vector 82 | /// 83 | /// ## Examples 84 | /// 85 | /// ``` 86 | /// use macos::foundation::{NSArray,NSString}; 87 | /// 88 | /// let items = vec!["abc","def","ghi"]; 89 | /// let array = NSArray::from_vec(items, |item| NSString::from(item)); 90 | /// assert_eq!(3, array.count()); 91 | /// ``` 92 | pub fn from_vec(items: Vec, transform: F) -> Self 93 | where F: Fn(&T) -> O, 94 | O: ObjCClass { 95 | let array = NSMutableArray::new(); 96 | for item in items { 97 | array.push(transform(&item)); 98 | } 99 | array.copy() 100 | } 101 | 102 | /// Create an array from a vector 103 | /// 104 | /// ## Examples 105 | /// 106 | /// ``` 107 | /// use macos::foundation::{NSArray,NSString}; 108 | /// 109 | /// let items = vec!["abc","def","ghi"]; 110 | /// let array = NSArray::from_vec(items, |item| NSString::from(item)); 111 | /// assert_eq!("def", array.get::(1).unwrap().as_str().unwrap()); 112 | /// ``` 113 | pub fn get(&self, index: NSUInteger) ->Option { 114 | if self.count() > index { 115 | T::from_ptr(unsafe { msg_send![self.ptr, objectAtIndex:index] }) 116 | } else { 117 | None 118 | } 119 | } 120 | 121 | pub fn index_of(&self, object: &T) -> NSUInteger { 122 | unsafe { msg_send![self.ptr, indexOfObject:object.ptr()] } 123 | } 124 | 125 | pub fn count(&self) -> NSUInteger { 126 | unsafe { msg_send![self.ptr, count] } 127 | } 128 | 129 | pub fn as_mut(&self) -> NSMutableArray { 130 | NSMutableArray { ptr: unsafe { msg_send![self.ptr, mutableCopy] }} 131 | } 132 | } 133 | 134 | impl NSAutoreleasePool { 135 | 136 | pub fn new() -> Self { 137 | NSAutoreleasePool { 138 | ptr: unsafe { msg_send![class!(NSAutoreleasePool), new] } 139 | } 140 | } 141 | 142 | pub fn drain(&self) { 143 | unsafe { msg_send![self.ptr, drain] } 144 | } 145 | } 146 | 147 | impl NSMutableArray { 148 | 149 | /// Create empty `NSMutableArray` 150 | /// 151 | /// ## Examples 152 | /// 153 | /// ``` 154 | /// use macos::foundation::NSMutableArray; 155 | /// 156 | /// let array = NSMutableArray::new(); 157 | /// assert_eq!(0, array.count()); 158 | /// ``` 159 | pub fn new() -> Self { 160 | NSMutableArray { ptr: unsafe { msg_send![class!(NSMutableArray), new] }} 161 | } 162 | 163 | pub fn count(&self) -> NSUInteger { 164 | unsafe { msg_send![self.ptr, count] } 165 | } 166 | 167 | /// Add an object to the array 168 | /// 169 | /// ## Examples 170 | /// 171 | /// ``` 172 | /// use macos::foundation::{NSMutableArray,NSString}; 173 | /// 174 | /// let array = NSMutableArray::new(); 175 | /// array.push(NSString::from("hello")); 176 | /// assert_eq!(1, array.count()); 177 | /// ``` 178 | pub fn push(&self, object: T) { 179 | unsafe { msg_send![self.ptr, addObject:object.ptr()] } 180 | } 181 | 182 | pub fn copy(&self) -> NSArray { 183 | let ptr = unsafe { msg_send![self.ptr, copy] }; 184 | NSArray { ptr: ptr } 185 | } 186 | } 187 | 188 | impl NSBundle { 189 | 190 | pub fn from_class(class: &Class) -> Option { 191 | NSBundle::from_ptr(unsafe { 192 | msg_send![class!(NSBundle), bundleForClass:class] 193 | }) 194 | } 195 | 196 | pub fn get_info_dict_object(&self, key: &str) -> Option { 197 | T::from_ptr(unsafe { 198 | msg_send![self.ptr, objectForInfoDictionaryKey:NSString::from(key).ptr] 199 | }) 200 | } 201 | } 202 | 203 | impl NSDictionary { 204 | 205 | /// Creates a reference to an object in a dictionary if the specified type 206 | /// matches the actual type 207 | pub fn get(&self, key: &str)-> Option { 208 | T::from_ptr(unsafe { self.raw_object_for_key(key) }) 209 | } 210 | 211 | unsafe fn raw_object_for_key(&self, key: &str) -> Id { 212 | msg_send![self.ptr, objectForKey:NSString::from(key).ptr] 213 | } 214 | } 215 | 216 | impl NSError { 217 | 218 | pub fn description(&self) -> Option { 219 | NSString::from_ptr(unsafe { msg_send![self.ptr, description] }) 220 | } 221 | 222 | pub fn localized_description(&self) -> Option { 223 | NSString::from_ptr(unsafe { msg_send![self.ptr, localizedDescription] }) 224 | } 225 | pub fn localized_failure_reason(&self) -> Option { 226 | NSString::from_ptr(unsafe { msg_send![self.ptr, localizedFailureReason] }) 227 | } 228 | 229 | } 230 | 231 | impl NSNotification { 232 | 233 | pub fn object(&self) -> Option { 234 | T::from_ptr(unsafe { msg_send![self.ptr, object] }) 235 | } 236 | 237 | pub fn user_info(&self) -> Option { 238 | NSDictionary::from_ptr(unsafe { msg_send![self.ptr, userInfo] }) 239 | } 240 | } 241 | 242 | impl NSNumber { 243 | 244 | pub fn integer_value(&self) -> NSInteger { 245 | unsafe { msg_send![self.ptr, integerValue] } 246 | } 247 | } 248 | 249 | #[repr(C)] 250 | pub struct NSOperatingSystemVersion { 251 | pub major_version: NSInteger, 252 | pub minor_version: NSInteger, 253 | pub patch_version: NSInteger, 254 | } 255 | 256 | unsafe impl Encode for NSOperatingSystemVersion { 257 | fn encode() -> Encoding { 258 | let encoding = format!("{{NSOperatingSystemVersion={}{}{}}}", 259 | NSInteger::encode().as_str(), 260 | NSInteger::encode().as_str(), 261 | NSInteger::encode().as_str()); 262 | unsafe { Encoding::from_str(&encoding) } 263 | } 264 | } 265 | 266 | impl NSProcessInfo { 267 | 268 | pub fn process_info() -> Self { 269 | NSProcessInfo { ptr: unsafe { 270 | msg_send![class!(NSProcessInfo), processInfo] 271 | }} 272 | } 273 | 274 | pub fn os_version(&self) -> NSOperatingSystemVersion { 275 | unsafe { msg_send![self.ptr, operatingSystemVersion] } 276 | } 277 | } 278 | 279 | impl NSString { 280 | 281 | /// Create a new empty `NSString` 282 | /// 283 | /// ## Examples 284 | /// 285 | /// ``` 286 | /// use macos::foundation::NSString; 287 | /// 288 | /// let string = NSString::new(); 289 | /// assert_eq!(0, string.len()); 290 | /// ``` 291 | pub fn new() -> Self { 292 | NSString { ptr: unsafe { msg_send![class!(NSString), new] } } 293 | } 294 | 295 | /// Creates an `NSString` from a `str`. 296 | /// 297 | /// ## Examples 298 | /// 299 | /// ``` 300 | /// use macos::foundation::NSString; 301 | /// 302 | /// let string = NSString::from("hello"); 303 | /// assert_eq!("hello", string.as_str().unwrap()); 304 | /// ``` 305 | pub fn from(content: &str) -> Self { 306 | let ptr: *mut Object = unsafe { 307 | let string: *mut Object = msg_send![class!(NSString), alloc]; 308 | msg_send![string, initWithBytes:content.as_ptr() 309 | length:content.len() 310 | encoding:UTF8_ENCODING] 311 | }; 312 | NSString { ptr: ptr } 313 | } 314 | 315 | /// Append one `NSString` to another 316 | /// 317 | /// ## Examples 318 | /// 319 | /// ``` 320 | /// use macos::foundation::NSString; 321 | /// 322 | /// assert_eq!("hi", NSString::from("hi").as_str().unwrap()); 323 | /// ``` 324 | pub fn push_str(&mut self, other: NSString) { 325 | let ptr: *mut Object = unsafe { 326 | msg_send![self.ptr, stringByAppendingString:other.ptr] 327 | }; 328 | self.ptr = ptr; 329 | } 330 | 331 | /// Coerce a `NSString` into a `str` 332 | /// 333 | /// ## Examples 334 | /// 335 | /// ``` 336 | /// use macos::foundation::NSString; 337 | /// 338 | /// let mut content = NSString::from("h"); 339 | /// content.push_str(NSString::from("i")); 340 | /// assert_eq!(NSString::from("hi"), content); 341 | /// ``` 342 | pub fn as_str<'a>(&self) -> Option<&'a str> { 343 | let bytes = unsafe { 344 | let byte_str = self.utf8() as *const u8; 345 | slice::from_raw_parts(byte_str, self.len()) 346 | }; 347 | str::from_utf8(bytes).ok() 348 | } 349 | 350 | /// A null-terminated UTF-8 representation 351 | pub fn utf8(&self) -> *const libc::c_char { 352 | unsafe { msg_send![self.ptr, UTF8String] } 353 | } 354 | 355 | /// The length of the string as measured in UTF-8 code points 356 | /// 357 | /// ## Examples 358 | /// 359 | /// ``` 360 | /// use macos::foundation::NSString; 361 | /// 362 | /// assert_eq!(0, NSString::new().len()); 363 | /// assert_eq!(5, NSString::from("hello").len()); 364 | /// ``` 365 | pub fn len(&self) -> usize { 366 | unsafe { msg_send![self.ptr, lengthOfBytesUsingEncoding:UTF8_ENCODING] } 367 | } 368 | } 369 | 370 | #[repr(C)] 371 | pub struct NSRange { 372 | pub location: NSUInteger, 373 | pub length: NSUInteger, 374 | } 375 | 376 | unsafe impl Encode for NSRange { 377 | fn encode() -> Encoding { 378 | let encoding = format!("{{NSRange={}{}}}", 379 | NSUInteger::encode().as_str(), 380 | NSUInteger::encode().as_str()); 381 | unsafe { Encoding::from_str(&encoding) } 382 | } 383 | } 384 | 385 | impl NSURL { 386 | 387 | /// Create a new `NSURL` from an `NSString` 388 | /// 389 | /// ## Examples 390 | /// 391 | /// ``` 392 | /// use macos::foundation::{NSString,NSURL}; 393 | /// 394 | /// let url = NSURL::from(NSString::from("/some/path")); 395 | /// assert_eq!("/some/path", url.absolute_string().as_str().unwrap()); 396 | /// ``` 397 | pub fn from(string: NSString) -> Self { 398 | let ptr: *mut Object = unsafe { 399 | msg_send![class!(NSURL), URLWithString:string.ptr] 400 | }; 401 | NSURL { ptr: ptr } 402 | } 403 | 404 | pub fn absolute_string(&self) -> NSString { 405 | let ptr: *mut Object = unsafe { msg_send![self.ptr, absoluteString] }; 406 | NSString { ptr: ptr } 407 | } 408 | 409 | pub fn scheme(&self) -> NSString { 410 | let ptr: *mut Object = unsafe { msg_send![self.ptr, scheme] }; 411 | NSString { ptr: ptr } 412 | } 413 | } 414 | 415 | impl NSURLRequest { 416 | 417 | pub fn from(url: NSURL) -> Self { 418 | let ptr: *mut Object = unsafe { 419 | msg_send![class!(NSURLRequest), requestWithURL:url.ptr] 420 | }; 421 | NSURLRequest { ptr: ptr } 422 | } 423 | 424 | pub fn url(&self) -> NSURL { 425 | NSURL { ptr: unsafe { msg_send![self.ptr, URL] } } 426 | } 427 | } 428 | 429 | #[test] 430 | fn nsstring_eq() { 431 | assert_eq!(NSString::from("hello"), NSString::from("hello")); 432 | assert_eq!(NSString::new(), NSString::new()); 433 | assert!(NSString::from("bat") != NSString::from("bot")); 434 | } 435 | -------------------------------------------------------------------------------- /macos/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate objc; 3 | extern crate block; 4 | extern crate libc; 5 | 6 | use objc::runtime::Object; 7 | 8 | 9 | pub type Id = *mut Object; 10 | #[allow(non_upper_case_globals)] 11 | pub const nil: Id = 0 as Id; 12 | 13 | pub trait ObjCClass: Sized { 14 | 15 | fn ptr(&self) -> Id; 16 | 17 | fn from_ptr(ptr: Id) -> Option; 18 | 19 | fn class_name() -> &'static str; 20 | 21 | fn nil() -> Self; 22 | 23 | fn ptr_is_class(ptr: Id) -> bool; 24 | 25 | // FIXME: replace with TryInto trait when stabilized 26 | fn coerce(&self) -> Option { 27 | T::from_ptr(self.ptr()) 28 | } 29 | 30 | fn autorelease(&self) -> Self; 31 | 32 | fn release(&mut self); 33 | } 34 | 35 | /// Implements the basics of `NSObject` 36 | #[macro_export] 37 | macro_rules! impl_objc_class { 38 | ($name: ident) => ( 39 | #[derive(Debug)] 40 | pub struct $name { ptr: Id } 41 | 42 | impl PartialEq for $name { 43 | fn eq(&self, other: &$name) -> bool { 44 | let eq: BOOL = unsafe { 45 | msg_send![self.ptr, isEqual:other.ptr] 46 | }; 47 | eq == YES 48 | } 49 | } 50 | 51 | impl ObjCClass for $name { 52 | 53 | fn ptr(&self) -> Id { self.ptr } 54 | 55 | fn from_ptr(ptr: Id) -> Option { 56 | match $name::ptr_is_class(ptr) { 57 | true => Some($name { ptr: ptr }), 58 | false => None 59 | } 60 | } 61 | 62 | fn ptr_is_class(ptr: Id) -> bool { 63 | let eq: BOOL = unsafe { 64 | msg_send![ptr, isKindOfClass:class!($name)] 65 | }; 66 | if eq != YES && ptr != 0 as Id { 67 | println!("ERROR! Failed type coercion to {}", $name::class_name()); 68 | } 69 | eq == YES 70 | } 71 | 72 | fn nil() -> Self { $name { ptr: 0 as Id } } 73 | 74 | fn class_name() -> &'static str { stringify!($name) } 75 | 76 | fn autorelease(&self) -> Self { 77 | $name { ptr: unsafe { msg_send![self.ptr, autorelease] } } 78 | } 79 | 80 | fn release(&mut self) { 81 | let () = unsafe { msg_send![self.ptr(), release] }; 82 | self.ptr = 0 as Id; 83 | } 84 | } 85 | ); 86 | } 87 | 88 | pub mod appkit; 89 | pub mod core_foundation; 90 | pub mod core_graphics; 91 | pub mod core_services; 92 | pub mod foundation; 93 | pub mod webkit; 94 | -------------------------------------------------------------------------------- /macos/src/webkit.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use objc::runtime::{YES,NO,BOOL}; 4 | use foundation::{NSString,NSURLRequest,NSURL,NSUInteger}; 5 | use core_graphics::CGRect; 6 | use block::Block; 7 | 8 | use super::{Id,ObjCClass,nil}; 9 | 10 | 11 | #[link(name = "WebKit", kind = "framework")] 12 | extern {} 13 | 14 | pub type ContentExtensionCompletionHandler = dyn Deref>; 15 | 16 | pub enum WKFindOptions { 17 | CaseInsensitive = 1 << 0, 18 | AtWordStarts = 1 << 1, 19 | TreatMedialCapitalAsWordStart = 1 << 2, 20 | Backwards = 1 << 3, 21 | WrapAround = 1 << 4, 22 | ShowOverlay = 1 << 5, 23 | ShowFindIndicator = 1 << 6, 24 | ShowHighlight = 1 << 7 25 | } 26 | 27 | pub enum WKNavigationActionPolicy { 28 | Cancel = 0, 29 | Allow = 1, 30 | } 31 | 32 | #[derive(PartialEq)] 33 | pub enum WKNavigationType { 34 | LinkActivated = 0, 35 | FormSubmitted = 1, 36 | BackForward = 2, 37 | Reload = 3, 38 | FormResubmitted = 4, 39 | Other = -1, 40 | } 41 | 42 | impl_objc_class!(WKFrameInfo); 43 | impl_objc_class!(WKNavigation); 44 | impl_objc_class!(WKNavigationAction); 45 | impl_objc_class!(WKPreferences); 46 | impl_objc_class!(WKUserContentController); 47 | impl_objc_class!(WKWebView); 48 | impl_objc_class!(WKWebViewConfiguration); 49 | impl_objc_class!(WKWebsiteDataStore); 50 | impl_objc_class!(_WKUserContentExtensionStore); 51 | impl_objc_class!(_WKUserContentFilter); 52 | impl_objc_class!(_WKUserStyleSheet); 53 | 54 | impl WKFrameInfo { 55 | 56 | pub fn is_main_frame(&self) -> bool { 57 | let is_main: BOOL = unsafe { msg_send![self.ptr, isMainFrame] }; 58 | is_main == YES 59 | } 60 | } 61 | 62 | impl WKNavigation { 63 | 64 | pub fn request(&self) -> Option { 65 | NSURLRequest::from_ptr(unsafe { msg_send![self.ptr, _request] }) 66 | } 67 | 68 | pub fn url_string(&self) -> Option { 69 | self.request() 70 | .and_then(|req| Some(req.url().absolute_string())) 71 | } 72 | } 73 | 74 | impl WKNavigationAction { 75 | 76 | pub fn request(&self) -> Option { 77 | NSURLRequest::from_ptr(unsafe { msg_send![self.ptr, request] }) 78 | } 79 | 80 | pub fn modifier_flags(&self) -> NSUInteger { 81 | unsafe { msg_send![self.ptr, modifierFlags] } 82 | } 83 | 84 | pub fn navigation_type(&self) -> WKNavigationType { 85 | unsafe { msg_send![self.ptr, navigationType] } 86 | } 87 | 88 | pub fn target_frame(&self) -> Option { 89 | WKFrameInfo::from_ptr(unsafe { msg_send![self.ptr, targetFrame] }) 90 | } 91 | 92 | pub fn source_frame(&self) -> Option { 93 | WKFrameInfo::from_ptr(unsafe { msg_send![self.ptr, sourceFrame] }) 94 | } 95 | } 96 | 97 | impl WKPreferences { 98 | 99 | pub fn set_javascript_enabled(&self, enabled: bool) { 100 | let value = if enabled { YES } else { NO }; 101 | unsafe { msg_send![self.ptr, setJavaScriptEnabled:value] } 102 | } 103 | 104 | pub fn set_plugins_enabled(&self, enabled: bool) { 105 | let value = if enabled { YES } else { NO }; 106 | unsafe { msg_send![self.ptr, setPlugInsEnabled:value] } 107 | } 108 | } 109 | 110 | impl WKUserContentController { 111 | 112 | pub fn add_user_content_filter(&self, filter: _WKUserContentFilter) { 113 | unsafe { msg_send![self.ptr, _addUserContentFilter:filter.ptr()] } 114 | } 115 | 116 | pub fn add_user_style_sheet(&self, stylesheet: _WKUserStyleSheet) { 117 | unsafe { msg_send![self.ptr, _addUserStyleSheet:stylesheet.ptr()] } 118 | } 119 | 120 | pub fn can_add_user_style_sheet(&self) -> bool { 121 | let responds: BOOL = unsafe { 122 | msg_send![self.ptr, respondsToSelector:sel!(_addUserStyleSheet:)] 123 | }; 124 | responds == YES 125 | } 126 | } 127 | 128 | impl WKWebView { 129 | 130 | pub fn new(frame: CGRect, config: WKWebViewConfiguration) -> Self { 131 | let ptr = unsafe { 132 | let webview: Id = msg_send![class!(WKWebView), alloc]; 133 | let webview: Id = msg_send![webview, initWithFrame:frame 134 | configuration:config.ptr()]; 135 | webview 136 | }; 137 | WKWebView { ptr: ptr } 138 | } 139 | 140 | pub fn load_request(&self, request: NSURLRequest) { 141 | unsafe { msg_send![self.ptr, loadRequest:request.ptr()] } 142 | } 143 | 144 | pub fn set_history_delegate(&self, delegate: T) { 145 | unsafe { msg_send![self.ptr, _setHistoryDelegate:delegate.ptr()] } 146 | } 147 | 148 | pub fn set_navigation_delegate(&self, delegate: T) { 149 | unsafe { msg_send![self.ptr, setNavigationDelegate:delegate.ptr()] } 150 | } 151 | 152 | pub fn configuration(&self) -> WKWebViewConfiguration { 153 | WKWebViewConfiguration { 154 | ptr: unsafe { msg_send![self.ptr, configuration] } 155 | } 156 | } 157 | 158 | pub fn can_go_back(&self) -> bool { 159 | let can: BOOL = unsafe { msg_send![self.ptr, canGoBack] }; 160 | can == YES 161 | } 162 | 163 | pub fn can_go_forward(&self) -> bool { 164 | let can: BOOL = unsafe { msg_send![self.ptr, canGoForward] }; 165 | can == YES 166 | } 167 | 168 | pub fn go_back(&self) { 169 | unsafe { msg_send![self.ptr, goBack] } 170 | } 171 | 172 | pub fn go_forward(&self) { 173 | unsafe { msg_send![self.ptr, goForward] } 174 | } 175 | 176 | pub fn reload(&self) { 177 | unsafe { msg_send![self.ptr, reload:nil] } 178 | } 179 | 180 | pub fn reload_without_content_blockers(&self) { 181 | if self.can_reload_without_content_blockers() { 182 | unsafe { msg_send![self.ptr, _reloadWithoutContentBlockers] } 183 | } 184 | } 185 | 186 | pub fn can_reload_without_content_blockers(&self) -> bool { 187 | let responds: BOOL = unsafe { 188 | let selector = sel!(_reloadWithoutContentBlockers); 189 | msg_send![self.ptr, respondsToSelector:selector] 190 | }; 191 | responds == YES 192 | } 193 | 194 | pub fn stop_loading(&self) { 195 | unsafe { msg_send![self.ptr, stopLoading] } 196 | } 197 | 198 | pub fn has_only_secure_content(&self) -> bool { 199 | let has: BOOL = unsafe { msg_send![self.ptr, hasOnlySecureContent] }; 200 | has == YES 201 | } 202 | 203 | pub fn load_html_string(&self, contents: &str, base_url: &str) { 204 | unsafe { 205 | msg_send![self.ptr, loadHTMLString:NSString::from(contents) 206 | baseURL:NSURL::from(NSString::from(base_url))] 207 | } 208 | } 209 | 210 | pub fn is_loading(&self) -> bool { 211 | let loading: BOOL = unsafe { msg_send![self.ptr, isLoading] }; 212 | loading == YES 213 | } 214 | 215 | pub fn url(&self) -> Option { 216 | NSURL::from_ptr(unsafe { msg_send![self.ptr, URL] }) 217 | } 218 | 219 | pub fn title(&self) -> Option { 220 | NSString::from_ptr(unsafe { msg_send![self.ptr, title] }) 221 | } 222 | 223 | pub fn set_custom_user_agent(&self, user_agent: &str) { 224 | unsafe { 225 | msg_send![self.ptr, setCustomUserAgent:NSString::from(user_agent)] 226 | } 227 | } 228 | 229 | pub fn custom_user_agent(&self) -> Option { 230 | NSString::from_ptr(unsafe { msg_send![self.ptr, customUserAgent] }) 231 | } 232 | 233 | pub fn evaluate_javascript(&self, script: &str) { 234 | unsafe { 235 | msg_send![self.ptr, evaluateJavaScript:NSString::from(script) 236 | completionHandler:nil] 237 | } 238 | } 239 | 240 | pub fn find_string(&self, query: &str) { 241 | let options: NSUInteger = WKFindOptions::CaseInsensitive as NSUInteger | 242 | WKFindOptions::WrapAround as NSUInteger | 243 | WKFindOptions::ShowFindIndicator as NSUInteger | 244 | WKFindOptions::TreatMedialCapitalAsWordStart as NSUInteger | 245 | WKFindOptions::ShowHighlight as NSUInteger; 246 | unsafe { 247 | msg_send![self.ptr, _findString:NSString::from(query) 248 | options:options 249 | maxCount:100 as NSUInteger] 250 | } 251 | } 252 | 253 | pub fn hide_find_results(&self) { 254 | unsafe { msg_send![self.ptr, _hideFindUI] } 255 | } 256 | 257 | pub fn remove_from_superview(&self) { 258 | unsafe { msg_send![self.ptr, removeFromSuperview] } 259 | } 260 | 261 | pub fn release_delegates(&self) { 262 | unsafe { 263 | let nav_delegate: Id = msg_send![self.ptr, navigationDelegate]; 264 | let () = msg_send![nav_delegate, release]; 265 | let history_delegate: Id = msg_send![self.ptr, _historyDelegate]; 266 | let () = msg_send![history_delegate, release]; 267 | let () = msg_send![self.ptr, setNavigationDelegate:nil]; 268 | let () = msg_send![self.ptr, _setHistoryDelegate:nil]; 269 | } 270 | } 271 | 272 | pub fn close(&self) { 273 | unsafe { msg_send![self.ptr, _close] } 274 | } 275 | } 276 | 277 | impl WKWebViewConfiguration { 278 | 279 | pub fn new() -> Self { 280 | WKWebViewConfiguration { 281 | ptr: unsafe { msg_send![class!(WKWebViewConfiguration), new] } 282 | } 283 | } 284 | 285 | pub fn preferences(&self) -> WKPreferences { 286 | WKPreferences { ptr: unsafe { msg_send![self.ptr, preferences] } } 287 | } 288 | 289 | pub fn user_content_controller(&self) -> WKUserContentController { 290 | WKUserContentController { 291 | ptr: unsafe { msg_send![self.ptr, userContentController] } 292 | } 293 | } 294 | 295 | pub fn website_data_store(&self) -> WKWebsiteDataStore { 296 | WKWebsiteDataStore { 297 | ptr: unsafe { msg_send![self.ptr, websiteDataStore] } 298 | } 299 | } 300 | 301 | pub fn set_website_data_store(&self, store: WKWebsiteDataStore) { 302 | unsafe { msg_send![self.ptr, setWebsiteDataStore:store.ptr()] } 303 | } 304 | } 305 | 306 | impl WKWebsiteDataStore { 307 | 308 | pub fn default_store() -> Self { 309 | WKWebsiteDataStore { 310 | ptr: unsafe { 311 | msg_send![class!(WKWebsiteDataStore), defaultDataStore] 312 | } 313 | } 314 | } 315 | 316 | pub fn nonpersistent_store() -> Self { 317 | WKWebsiteDataStore { 318 | ptr: unsafe { 319 | msg_send![class!(WKWebsiteDataStore), nonPersistentDataStore] 320 | } 321 | } 322 | } 323 | } 324 | 325 | impl _WKUserContentExtensionStore { 326 | 327 | pub fn default_store() -> Self { 328 | _WKUserContentExtensionStore { 329 | ptr: unsafe { 330 | msg_send![class!(_WKUserContentExtensionStore), defaultStore] 331 | } 332 | } 333 | } 334 | 335 | pub fn compile_content_extension(&self, 336 | identifier: &str, 337 | extension: &str, 338 | block: &ContentExtensionCompletionHandler) { 339 | let id_str = NSString::from(identifier); 340 | let ex_str = NSString::from(extension); 341 | unsafe { 342 | msg_send![self.ptr, compileContentExtensionForIdentifier:id_str 343 | encodedContentExtension:ex_str 344 | completionHandler:block.deref()] 345 | } 346 | } 347 | 348 | pub fn lookup_content_extension(&self, 349 | identifier: &str, 350 | block: &ContentExtensionCompletionHandler) { 351 | let id_str = NSString::from(identifier); 352 | unsafe { 353 | msg_send![self.ptr, lookupContentExtensionForIdentifier:id_str 354 | completionHandler:block.deref()] 355 | } 356 | } 357 | } 358 | 359 | impl _WKUserStyleSheet { 360 | 361 | pub fn new(styles: &str) -> Self { 362 | let source = NSString::from(styles); 363 | let ptr = unsafe { 364 | let sheet: Id = msg_send![class!(_WKUserStyleSheet), alloc]; 365 | let sheet: Id = msg_send![sheet, initWithSource:source 366 | forMainFrameOnly:YES]; 367 | sheet 368 | }; 369 | _WKUserStyleSheet { ptr: ptr } 370 | } 371 | } 372 | -------------------------------------------------------------------------------- /src/command.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use std::fs::{File,metadata,read_dir}; 3 | 4 | use config::Config; 5 | use ui::BrowserConfiguration; 6 | 7 | /// A representation of a script which executes and returns a boolean value 8 | /// indicating success 9 | #[derive(Debug,Clone)] 10 | pub struct Command { 11 | pub path: String, 12 | pub arguments: Vec, 13 | } 14 | 15 | impl Command { 16 | 17 | /// Parse a command name and arguments into an instance of Command 18 | pub fn parse(input: &str, config: &Config, suffix: &str) -> Option { 19 | let mut components = input.split_whitespace(); 20 | components.next() 21 | .and_then(|name| config.resolved_command_name(name)) 22 | .and_then(|name| resolve_command(config.command_search_paths(), &name, suffix)) 23 | .and_then(|path| { 24 | Some(Command { 25 | path: path, 26 | arguments: components.map(|arg| String::from(arg)).collect(), 27 | }) 28 | }) 29 | } 30 | 31 | pub fn list_commands(prefix: &str, config: &Config) -> Vec { 32 | let mut entries: Vec = vec![]; 33 | for search_path in config.command_search_paths() { 34 | if let Ok(contents) = read_dir(search_path) { 35 | for entry in contents { 36 | if let Ok(entry) = entry { 37 | let path = entry.path(); 38 | if path.is_file() { 39 | if let Some(stem) = path.file_stem().and_then(|p| p.to_str()) { 40 | if stem.starts_with(prefix) { 41 | entries.push(String::from(stem)); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | entries.sort(); 50 | entries 51 | } 52 | 53 | /// A File handle to the command path 54 | pub fn file(&self) -> Option { 55 | File::open(&self.path).ok() 56 | } 57 | } 58 | 59 | /// Iterate over search paths returning the first file path in search paths 60 | /// with the provided name 61 | fn resolve_command(search_paths: Vec, name: &str, suffix: &str) -> Option { 62 | if name.is_empty() { 63 | return None 64 | } 65 | let mut ordered_paths: Vec = search_paths.iter() 66 | .filter_map(|path| join_paths(&path, &format!("{}.{}", name, suffix))) 67 | .filter(|path| metadata(&path).is_ok()) 68 | .collect(); 69 | ordered_paths.reverse(); 70 | return ordered_paths.pop(); 71 | } 72 | 73 | /// Join a directory and file name into a string path if possible 74 | fn join_paths(dir: &str, file_name: &str) -> Option { 75 | let buf = Path::new(dir).join(file_name); 76 | buf.to_str().and_then(|path| Some(String::from(path))) 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use config::Config; 83 | use ui::BrowserConfiguration; 84 | use std::env::temp_dir; 85 | use std::fs::{File,remove_file}; 86 | use std::io::Write; 87 | use std::path::Path; 88 | 89 | #[test] 90 | #[allow(unused_must_use)] 91 | fn resolve_by_filename() { 92 | let (path, result) = create_command("hello.lua", 93 | b"print(\"hello world\");", 94 | "hello world"); 95 | remove_file(Path::new(path.as_str().clone())); 96 | assert!(result.is_some()); 97 | 98 | let command = result.unwrap(); 99 | assert_eq!(command.arguments, vec![String::from("world")]); 100 | assert_eq!(command.path, path); 101 | } 102 | 103 | #[allow(unused_must_use)] 104 | fn create_command(name: &str, content: &[u8], invocation: &str) -> (String, Option) { 105 | let dir = temp_dir(); 106 | let search_path = String::from(dir.clone().to_str().unwrap()); 107 | let file_path = Path::new(dir.to_str().unwrap()).join(name); 108 | let mut file = File::create(file_path.as_path()).ok().unwrap(); 109 | assert!(file.write(content).is_ok()); 110 | file.flush(); 111 | let config = Config::parse(&format!(r#" 112 | [commands] 113 | search-paths = ["{}"] 114 | "#, search_path)).unwrap(); 115 | let result = Command::parse(invocation, &config, "lua"); 116 | return (String::from(file_path.to_str().unwrap()), result); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | //! Configuration manipulation and handling for common browser options 2 | use std::fs::File; 3 | use std::io::Read; 4 | use std::collections::HashMap; 5 | 6 | use toml::Value; 7 | 8 | use ui::BrowserConfiguration; 9 | 10 | 11 | pub const DEFAULT_CONFIG: &'static str = r#" 12 | [general] 13 | private-browsing = false 14 | 15 | [window] 16 | start-page = "https://delisa.fuller.li" 17 | 18 | [new-frame] 19 | opens-in-focused-window = false 20 | "#; 21 | 22 | /// Placeholder used in webkitten configuration to represent the configuration 23 | /// property `general.config-dir`. 24 | const CONFIG_DIR: &'static str = "CONFIG_DIR"; 25 | 26 | const HOME: &'static str = "HOME"; 27 | 28 | /// Configuration option storage and lookup 29 | /// 30 | /// ## Examples 31 | /// 32 | /// Using a start page setting for new windows 33 | /// 34 | /// ``` 35 | /// use webkitten::config::Config; 36 | /// use webkitten::ui::BrowserConfiguration; 37 | /// 38 | /// let config = Config::parse(r#" 39 | /// [window] 40 | /// start-page = "file:///path/to/the/page.html" 41 | /// "#).unwrap(); 42 | /// let start_page = config.start_page().unwrap(); 43 | /// assert_eq!("file:///path/to/the/page.html", &start_page); 44 | /// ``` 45 | /// 46 | /// Replacing `CONFIG_DIR` in string options with a preferred path 47 | /// 48 | /// ``` 49 | /// use webkitten::config::Config; 50 | /// use webkitten::ui::BrowserConfiguration; 51 | /// 52 | /// let config = Config::parse(r#" 53 | /// [general] 54 | /// config-dir = "/path/to/config" 55 | /// [window] 56 | /// start-page = "file://CONFIG_DIR/page.html" 57 | /// "#).unwrap(); 58 | /// let start_page = config.start_page().unwrap(); 59 | /// assert_eq!("file:///path/to/config/page.html", &start_page); 60 | /// ``` 61 | /// 62 | /// Looking up a custom field 63 | /// 64 | /// ``` 65 | /// use webkitten::config::Config; 66 | /// use webkitten::ui::BrowserConfiguration; 67 | /// 68 | /// let config = Config::parse(r#" 69 | /// [dependencies] 70 | /// external-path = "/path/to/bin" 71 | /// "#).unwrap(); 72 | /// let path = config.lookup_str("dependencies.external-path").unwrap(); 73 | /// assert_eq!("/path/to/bin", &path); 74 | /// ``` 75 | /// 76 | /// Overriding a field with site-specific options 77 | /// 78 | /// ``` 79 | /// use webkitten::config::Config; 80 | /// use webkitten::ui::BrowserConfiguration; 81 | /// 82 | /// let config = Config::parse(r#" 83 | /// [dessert] 84 | /// pie = false 85 | /// [sites."example.us".dessert] 86 | /// pie = true 87 | /// "#).unwrap(); 88 | /// let pie = config.lookup_site_bool("http://example.us/other/stuff.html", 89 | /// "dessert.pie"); 90 | /// assert_eq!(true, pie.unwrap()); 91 | /// ``` 92 | /// 93 | /// Using global fallback for a missing site-specific option 94 | /// 95 | /// ``` 96 | /// use webkitten::config::Config; 97 | /// use webkitten::ui::BrowserConfiguration; 98 | /// 99 | /// let config = Config::parse(r#" 100 | /// [dependencies] 101 | /// external-path = "/path/to/bin" 102 | /// [sites."example.com".dependencies] 103 | /// external-path = "/path/to/sites-bin" 104 | /// "#).unwrap(); 105 | /// let path = config.lookup_site_str("http://example.co.uk/old", "dependencies.external-path"); 106 | /// assert_eq!("/path/to/bin", &path.unwrap()); 107 | /// ``` 108 | pub struct Config { 109 | value: Value 110 | } 111 | 112 | impl BrowserConfiguration for Config { 113 | 114 | fn parse(raw_input: &str) -> Option { 115 | match raw_input.parse() { 116 | Ok(value) => Some(Config { value: value }), 117 | Err(errors) => { 118 | for err in errors { error!("Failed to parse toml: {}", err); } 119 | None 120 | }, 121 | } 122 | } 123 | 124 | fn lookup_bool<'a>(&'a self, key: &'a str) -> Option { 125 | self.lookup(key) 126 | .and_then(|value| value.as_bool()) 127 | } 128 | 129 | fn lookup_raw_str<'a>(&'a self, key: &'a str) -> Option { 130 | self.lookup(key) 131 | .and_then(|value| value.as_str()) 132 | .and_then(|value| Some(String::from(value))) 133 | } 134 | 135 | fn lookup_str<'a>(&'a self, key: &'a str) -> Option { 136 | self.lookup_raw_str(key) 137 | .and_then(|value| Some(self.parse_path(&value))) 138 | } 139 | 140 | fn lookup_integer<'a>(&'a self, key: &'a str) -> Option { 141 | self.lookup(key) 142 | .and_then(|value| value.as_integer()) 143 | } 144 | 145 | fn lookup_str_table(&self, key: &str) -> Option> { 146 | if let Some(table) = self.lookup(key).and_then(|value| value.as_table()) { 147 | let mut map: HashMap = HashMap::new(); 148 | for (key, raw_value) in table { 149 | if let Some(value) = raw_value.as_str() { 150 | map.insert(key.to_owned(), value.to_owned()); 151 | } 152 | } 153 | return Some(map); 154 | } 155 | None 156 | } 157 | 158 | fn lookup_str_vec(&self, key: &str) -> Option> { 159 | self.lookup(key) 160 | .and_then(|value| value.as_slice()) 161 | .and_then(|values| { 162 | let mut str_values: Vec = vec![]; 163 | for value in values { 164 | if let Some(value) = value.as_str() { 165 | str_values.push(self.parse_path(value)) 166 | } 167 | } 168 | Some(str_values) 169 | }) 170 | } 171 | } 172 | 173 | impl Config { 174 | 175 | /// Create a `Configuration` with the default options 176 | pub fn default() -> Option { 177 | Config::parse(DEFAULT_CONFIG) 178 | } 179 | 180 | /// Reload cached configuration from disk returns true if parsing is 181 | /// successful 182 | pub fn load(&mut self, path: &str) -> bool { 183 | if let Some(update) = Config::parse_file(path) { 184 | self.value = update.value; 185 | true 186 | } else { 187 | false 188 | } 189 | } 190 | 191 | /// Parse a file at a path and create a `Configuration` if possible 192 | pub fn parse_file(path: &str) -> Option { 193 | let mut buffer = String::new(); 194 | File::open(path).ok() 195 | .and_then(|mut file| file.read_to_string(&mut buffer).ok()) 196 | .and_then(|_| Config::parse(buffer.as_str())) 197 | } 198 | 199 | /// Look up the raw TOML value for a key 200 | fn lookup<'a>(&'a self, key: &'a str) -> Option<&Value> { 201 | self.value.lookup(&key.clone()) 202 | } 203 | 204 | fn parse_path(&self, value: &str) -> String { 205 | self.replace_config_dir(&self.replace_home(value)) 206 | } 207 | 208 | fn replace_config_dir<'a>(&self, value: &'a str) -> String { 209 | self.config_dir() 210 | .and_then(|dir| Some(value.replace(CONFIG_DIR, &dir))) 211 | .unwrap_or(String::from(value)) 212 | } 213 | 214 | fn replace_home(&self, value: &str) -> String { 215 | if let Some(home) = dirs::home_dir() { 216 | if let Some(home) = home.to_str() { 217 | return value.replace(HOME, &home) 218 | } 219 | } 220 | String::from(value) 221 | } 222 | } 223 | 224 | #[cfg(test)] 225 | mod tests { 226 | 227 | use super::Config; 228 | use ui::{BrowserConfiguration,BufferEvent}; 229 | 230 | #[test] 231 | fn lookup_fail_uri_commands() { 232 | let config = Config::parse(r#" 233 | [commands] 234 | on-fail-uri = ["bob","refresh"] 235 | "#).unwrap(); 236 | let commands = config.on_buffer_event_commands(&BufferEvent::Fail(String::new())); 237 | assert_eq!(2, commands.len()); 238 | assert_eq!(String::from("bob"), commands[0]); 239 | assert_eq!(String::from("refresh"), commands[1]); 240 | } 241 | 242 | #[test] 243 | fn lookup_request_uri_commands() { 244 | let config = Config::parse(r#" 245 | [commands] 246 | on-request-uri = ["bob","refresh"] 247 | "#).unwrap(); 248 | let commands = config.on_buffer_event_commands(&BufferEvent::Request); 249 | assert_eq!(2, commands.len()); 250 | assert_eq!(String::from("bob"), commands[0]); 251 | assert_eq!(String::from("refresh"), commands[1]); 252 | } 253 | 254 | #[test] 255 | fn lookup_load_uri_commands() { 256 | let config = Config::parse(r#" 257 | [commands] 258 | on-load-uri = ["bob","refresh"] 259 | "#).unwrap(); 260 | let commands = config.on_buffer_event_commands(&BufferEvent::Load); 261 | assert_eq!(2, commands.len()); 262 | assert_eq!(String::from("bob"), commands[0]); 263 | assert_eq!(String::from("refresh"), commands[1]); 264 | } 265 | 266 | #[test] 267 | fn lookup_site_override_vec() { 268 | let config = Config::parse(r#" 269 | [commands] 270 | on-load-uri = ["bob","refresh"] 271 | [sites."example.com".commands] 272 | on-load-uri = ["frut"] 273 | "#).unwrap(); 274 | let commands = config.lookup_site_str_vec("example.com/page.html", 275 | "commands.on-load-uri").unwrap(); 276 | assert_eq!(1, commands.len()); 277 | assert_eq!(String::from("frut"), commands[0]); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/keybinding.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt; 3 | 4 | pub enum KeyMask { 5 | Shift = 1 << 17, 6 | Control = 1 << 18, 7 | Alternate = 1 << 19, 8 | Super = 1 << 20, 9 | Function = 1 << 23, 10 | } 11 | 12 | const SHIFT_CODES: [&'static str; 1] = ["shift"]; 13 | const CONTROL_CODES: [&'static str; 2] = ["control", "ctrl"]; 14 | const ALT_CODES: [&'static str; 3] = ["alt", "option", "opt"]; 15 | const SUPER_CODES: [&'static str; 3] = ["super", "cmd", "command"]; 16 | const FN_CODES: [&'static str; 2] = ["fn", "function"]; 17 | const SPACE_CODE: &'static str = "space"; 18 | 19 | #[derive(Debug,PartialEq)] 20 | pub struct ParseError { 21 | message: String, 22 | reason: ParseErrorReason, 23 | } 24 | 25 | #[derive(Debug,PartialEq,Clone,Copy)] 26 | pub enum ParseErrorReason { 27 | ModifierNotFound, 28 | NoKeySpecified, 29 | NoModifierSpecified, 30 | TooManyKeysSpecified, 31 | } 32 | 33 | impl error::Error for ParseError { 34 | 35 | fn description(&self) -> &str { 36 | &self.message 37 | } 38 | } 39 | 40 | impl ParseError { 41 | 42 | fn new(reason: ParseErrorReason, 43 | keys: Vec<&str>, 44 | modifier: &str) -> ParseError { 45 | let message = match reason { 46 | ParseErrorReason::ModifierNotFound => 47 | format!("Modifier not found for '{}'", modifier), 48 | ParseErrorReason::NoKeySpecified => 49 | format!("No key specified in chord"), 50 | ParseErrorReason::NoModifierSpecified => 51 | format!("No modifier specified in chord"), 52 | ParseErrorReason::TooManyKeysSpecified => 53 | format!("Too many keys specified in chord: {}", keys.join(" ")), 54 | }; 55 | ParseError { reason: reason, message: message } 56 | } 57 | } 58 | 59 | impl fmt::Display for ParseError { 60 | 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | write!(f, "{}", self.message) 63 | } 64 | } 65 | 66 | pub fn parse(chord: &str) -> Result<(char, usize), ParseError> { 67 | let mut key: Option<&str> = None; 68 | let mut modifier: usize = 0; 69 | for component in chord.split(" ") { 70 | if component.len() == 1 || component == SPACE_CODE { 71 | if key.is_some() { 72 | return Err(ParseError::new(ParseErrorReason::TooManyKeysSpecified, 73 | vec![key.unwrap(), component], 74 | "")); 75 | } else if component == SPACE_CODE { 76 | key = Some(" "); 77 | } else { 78 | key = Some(component); 79 | } 80 | } else { 81 | let code = component.to_lowercase(); 82 | if ALT_CODES.contains(&code.as_str()) { 83 | modifier |= KeyMask::Alternate as usize; 84 | } else if SUPER_CODES.contains(&code.as_str()) { 85 | modifier |= KeyMask::Super as usize; 86 | } else if SHIFT_CODES.contains(&code.as_str()) { 87 | modifier |= KeyMask::Shift as usize; 88 | } else if CONTROL_CODES.contains(&code.as_str()) { 89 | modifier |= KeyMask::Control as usize; 90 | } else if FN_CODES.contains(&code.as_str()) { 91 | modifier |= KeyMask::Function as usize; 92 | } else { 93 | return Err(ParseError::new(ParseErrorReason::ModifierNotFound, 94 | vec![], 95 | component)); 96 | } 97 | } 98 | } 99 | 100 | if key.is_none() { 101 | Err(ParseError::new(ParseErrorReason::NoKeySpecified, vec![], "")) 102 | } else if modifier == 0 { 103 | Err(ParseError::new(ParseErrorReason::NoModifierSpecified, vec![], "")) 104 | } else { 105 | Ok((key.unwrap().chars().next().unwrap(), modifier)) 106 | } 107 | } 108 | 109 | #[cfg(test)] 110 | mod test { 111 | use super::*; 112 | 113 | #[test] 114 | fn parse_invalid_modifier_reason() { 115 | let chord = parse("Hyper 7"); 116 | assert_eq!(ParseErrorReason::ModifierNotFound, 117 | chord.err().unwrap().reason); 118 | } 119 | 120 | #[test] 121 | fn parse_invalid_modifier_modifier() { 122 | let chord = parse("Hyper 7"); 123 | assert!(chord.err().unwrap().message.contains("Hyper")); 124 | } 125 | 126 | #[test] 127 | fn parse_too_many_characters_reason() { 128 | let chord = parse("Ctrl Alt d a"); 129 | assert_eq!(ParseErrorReason::TooManyKeysSpecified, 130 | chord.err().unwrap().reason); 131 | } 132 | 133 | #[test] 134 | fn parse_no_modifier_reason() { 135 | let chord = parse("a"); 136 | assert_eq!(ParseErrorReason::NoModifierSpecified, 137 | chord.err().unwrap().reason); 138 | } 139 | 140 | #[test] 141 | fn parse_no_key_reason() { 142 | let chord = parse("Ctrl Alt"); 143 | assert_eq!(ParseErrorReason::NoKeySpecified, 144 | chord.err().unwrap().reason); 145 | } 146 | 147 | #[test] 148 | fn parse_case_insensitive_modifiers() { 149 | let chord1 = parse("CTRL OPTION a"); 150 | let chord2 = parse("Ctrl option a"); 151 | assert_eq!(chord1, chord2); 152 | 153 | let value = chord1.ok().unwrap(); 154 | assert_eq!('a', value.0); 155 | assert_eq!(KeyMask::Control as usize | KeyMask::Alternate as usize, 156 | value.1); 157 | } 158 | 159 | #[test] 160 | fn parse_alt_option_modifiers() { 161 | let chord1 = parse("Option b"); 162 | let chord2 = parse("Alt b"); 163 | assert_eq!(chord1, chord2); 164 | 165 | let value = chord1.ok().unwrap(); 166 | assert_eq!('b', value.0); 167 | assert_eq!(KeyMask::Alternate as usize, value.1); 168 | } 169 | 170 | #[test] 171 | fn parse_command_modifier() { 172 | assert_eq!(KeyMask::Super as usize, 173 | parse("cmd 1").ok().unwrap().1); 174 | assert_eq!(KeyMask::Super as usize, 175 | parse("command 1").ok().unwrap().1); 176 | assert_eq!(KeyMask::Super as usize, 177 | parse("super 1").ok().unwrap().1); 178 | } 179 | 180 | #[test] 181 | fn parse_alt_modifier() { 182 | assert_eq!(KeyMask::Alternate as usize, 183 | parse("alt 1").ok().unwrap().1); 184 | assert_eq!(KeyMask::Alternate as usize, 185 | parse("opt 1").ok().unwrap().1); 186 | assert_eq!(KeyMask::Alternate as usize, 187 | parse("option 1").ok().unwrap().1); 188 | } 189 | 190 | #[test] 191 | fn parse_function_modifier() { 192 | assert_eq!(KeyMask::Function as usize, 193 | parse("fn 1").ok().unwrap().1); 194 | assert_eq!(KeyMask::Function as usize, 195 | parse("function 1").ok().unwrap().1); 196 | } 197 | 198 | #[test] 199 | fn parse_control_modifier() { 200 | assert_eq!(KeyMask::Control as usize, 201 | parse("ctrl 1").ok().unwrap().1); 202 | assert_eq!(KeyMask::Control as usize, 203 | parse("control 1").ok().unwrap().1); 204 | } 205 | 206 | #[test] 207 | fn parse_space_key() { 208 | assert_eq!(' ', parse("space control").ok().unwrap().0); 209 | } 210 | 211 | #[test] 212 | fn parse_shift_modifier() { 213 | assert_eq!(KeyMask::Shift as usize, 214 | parse("shift 1").ok().unwrap().1); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate url; 2 | extern crate toml; 3 | extern crate getopts; 4 | #[macro_use] 5 | extern crate log; 6 | extern crate dirs; 7 | 8 | pub mod command; 9 | pub mod config; 10 | pub mod ui; 11 | pub mod optparse; 12 | pub mod script; 13 | mod keybinding; 14 | 15 | use ui::*; 16 | use script::ScriptingEngine; 17 | 18 | /// Application identifier for apps built with webkitten core 19 | pub const WEBKITTEN_APP_ID: &'static str = "me.delisa.Webkitten"; 20 | /// Application title for apps built with webkitten core 21 | pub const WEBKITTEN_TITLE: &'static str = "Webkitten"; 22 | 23 | /// The core of a webkitten application. The engine handles configuration options 24 | /// and responding to lifecycle and user events from the UI. 25 | pub struct Engine { 26 | pub config: config::Config, 27 | run_config: optparse::RunConfiguration, 28 | } 29 | 30 | impl Engine { 31 | 32 | /// Create a new application engine 33 | pub fn new(runtime: optparse::RunConfiguration) -> Option { 34 | config::Config::parse_file(&runtime.path).and_then(|config| { 35 | info!("Creating application engine with config path: {}", &runtime.path); 36 | Some(Engine { 37 | config: config, 38 | run_config: runtime 39 | }) 40 | }) 41 | } 42 | 43 | /// Any arguments specified at launch to be opened 44 | pub fn initial_pages<'a>(&'a self) -> &'a Vec { 45 | &self.run_config.start_pages 46 | } 47 | 48 | /// Reload configuration from path 49 | pub fn reload(&mut self) -> bool { 50 | self.config.load(&self.run_config.path) 51 | } 52 | 53 | fn use_argument_completion(&self, prefix: &str) -> bool { 54 | prefix.contains(" ") 55 | } 56 | } 57 | 58 | impl EventHandler for Engine { 59 | 60 | fn on_new_frame_request(&self, ui: &T, window_index: u32, uri: &str) 61 | where T: ApplicationUI, 62 | S: ScriptingEngine { 63 | if self.config.new_frame_uses_focused_window() { 64 | ui.open_webview::<_, config::Config>(window_index, Some(uri), None); 65 | } else { 66 | ui.open_window::<_, config::Config>(Some(uri), None); 67 | } 68 | } 69 | 70 | fn execute_command(&self, ui: &T, window_index: Option, text: &str) 71 | where T: ApplicationUI, 72 | S: ScriptingEngine { 73 | if let Some(text) = self.config.command_matching_prefix(text) { 74 | return self.execute_command(ui, window_index, &text); 75 | } else if let Some(command) = command::Command::parse(text, &self.config, S::file_extension()) { 76 | info!("Found command match: {}", command.path); 77 | if let Some(file) = command.file() { 78 | match S::execute::(file, command.arguments, ui, &self.run_config.path) { 79 | Err(err) => warn!("{}", err), 80 | Ok(success) => if let (true, Some(index)) = (success, window_index) { 81 | ui.set_command_field_text(index, "") 82 | } 83 | } 84 | } 85 | } else if let Some(default) = self.config.default_command() { 86 | if !text.starts_with(&default) { 87 | let mut command = String::from(default); 88 | command.push_str(" "); 89 | command.push_str(text); 90 | info!("Running the default command: {}", command); 91 | return self.execute_command(ui, window_index, &command); 92 | } 93 | } 94 | } 95 | 96 | fn close(&self, _ui: &T) 97 | where T: ApplicationUI, 98 | S: ScriptingEngine {} 99 | 100 | fn command_completions(&self, ui: &T, prefix: &str) -> Vec 101 | where T: ApplicationUI, 102 | S: ScriptingEngine { 103 | if self.use_argument_completion(prefix) { 104 | if let Some(command) = command::Command::parse(prefix, &self.config, S::file_extension()) { 105 | info!("Found command match for completion: {}", prefix); 106 | if let Some(file) = command.file() { 107 | info!("Completing command text using {}", command.path); 108 | return match S::autocomplete::(file, command.arguments, prefix, ui, &self.run_config.path) { 109 | Err(err) => { 110 | warn!("{}", err); 111 | vec![] 112 | }, 113 | Ok(completions) => completions 114 | } 115 | } 116 | } 117 | } 118 | command::Command::list_commands(prefix, &self.config) 119 | } 120 | 121 | fn on_buffer_event(&self, ui: &T, window_index: u32, webview_index: u32, uri: Option<&str>, event: BufferEvent) 122 | where T: ApplicationUI, 123 | S: ScriptingEngine { 124 | for name in self.config.on_buffer_event_commands(&event) { 125 | if let Some(command) = command::Command::parse(&name, &self.config, S::file_extension()) { 126 | if let Some(file) = command.file() { 127 | match S::on_buffer_event::(file, ui, &self.run_config.path, window_index, webview_index, uri, &event) { 128 | Err(err) => warn!("{}", err), 129 | Ok(_) => (), 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/optparse.rs: -------------------------------------------------------------------------------- 1 | use std::{env,fs}; 2 | use std::io::Write; 3 | use std::path::Path; 4 | use getopts::Options; 5 | use super::config; 6 | 7 | /// The runtime configuration of an instance of a webkitten application 8 | pub struct RunConfiguration { 9 | /// The configuration file path 10 | pub path: String, 11 | /// Pages to open on initial load 12 | pub start_pages: Vec, 13 | /// The exit status, set if the application has completed execution 14 | pub exit_status: Option<(i32, String)> 15 | } 16 | 17 | pub fn parse_opts(default_config_path: &str) -> RunConfiguration { 18 | let args: Vec = env::args().collect(); 19 | let mut opts = Options::new(); 20 | let mut exit_status: Option<(i32, String)> = None; 21 | let program = args[0].clone(); 22 | opts.optopt("c", "config", "Set the configuration path", "PATH"); 23 | opts.optflag("h", "help", "Print this help text"); 24 | match opts.parse(&args[1..]) { 25 | Ok(matches) => { 26 | let path = matches.opt_str("c") 27 | .unwrap_or(String::from(default_config_path)); 28 | validate_config_path(&path); 29 | if matches.opt_present("h") { 30 | exit_status = Some((0, usage(program, opts))); 31 | } 32 | RunConfiguration { 33 | path: path, 34 | start_pages: matches.free, 35 | exit_status: exit_status 36 | } 37 | }, 38 | Err(err) => { 39 | let message = format!("{}\n{}", err, usage(program, opts)); 40 | RunConfiguration { 41 | path: String::from(default_config_path), 42 | start_pages: vec![], 43 | exit_status: Some((1, message)) 44 | } 45 | } 46 | } 47 | } 48 | 49 | fn usage(program: String, opts: Options) -> String { 50 | let brief = format!("Usage: {} [options] [URI ...]", program); 51 | return opts.usage(&brief); 52 | } 53 | 54 | /// Check if the given config path exists, creating if not required to already 55 | /// exist 56 | fn validate_config_path(config_path: &str) { 57 | if let Some(parent) = Path::new(config_path).parent() { 58 | let metadata = fs::metadata(parent); 59 | if metadata.is_err() || !metadata.unwrap().is_dir() { 60 | write_config_dir(parent); 61 | } 62 | } 63 | if !fs::metadata(config_path).is_ok() { 64 | write_default_config(config_path); 65 | } 66 | } 67 | 68 | fn write_config_dir(path: &Path) { 69 | match fs::create_dir_all(path) { 70 | Ok(()) => (), 71 | Err(e) => panic!("Unable to create config dir ({}): {}", path.display(), e) 72 | } 73 | } 74 | 75 | fn write_default_config(config_path: &str) { 76 | match fs::File::create(config_path) { 77 | Ok(ref mut file) => { 78 | if file.write(config::DEFAULT_CONFIG.as_bytes()).is_ok() { 79 | let result = file.flush(); 80 | if result.is_err() { 81 | panic!("Unable to create default config ({}): {}", 82 | config_path, 83 | result.err().unwrap()); 84 | } 85 | } 86 | }, 87 | Err(e) => panic!("Unable to create default config ({}): {}", config_path, e) 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/script/mod.rs: -------------------------------------------------------------------------------- 1 | mod lua; 2 | 3 | pub use self::lua::LuaEngine; 4 | 5 | use std::error::Error; 6 | use std::fs::File; 7 | use std::fmt; 8 | 9 | use super::ui::{ApplicationUI,BufferEvent}; 10 | 11 | /// A sentinel value for representing empty optional numbers to scripting 12 | /// languages without optionals 13 | pub const NOT_FOUND: u32 = 483600; 14 | 15 | pub type ScriptResult = Result; 16 | 17 | #[derive(Debug)] 18 | pub struct ScriptError { 19 | description: String 20 | } 21 | 22 | impl Error for ScriptError { 23 | 24 | fn description(&self) -> &str { 25 | &self.description 26 | } 27 | 28 | fn cause(&self) -> Option<&dyn Error> { 29 | None 30 | } 31 | } 32 | 33 | impl fmt::Display for ScriptError { 34 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 35 | write!(f, "{}", self.description) 36 | } 37 | } 38 | 39 | /// A scripting runtime and event handler capable of evaluating file contents 40 | /// within the runtime, converting between internal runtime types and Rust 41 | /// types, and providing an interface to interaction with the UI. 42 | pub trait ScriptingEngine { 43 | 44 | /// The file extension to use when searching for command matches for this 45 | /// engine 46 | fn file_extension() -> &'static str; 47 | 48 | /// Evaluate the contents of a file withn the scripting runtime and execute 49 | /// the description event trigger 50 | fn describe(file: File) -> ScriptResult; 51 | 52 | /// Evaluate the contents of a file within the scripting runtime and execute 53 | /// the event trigger for running a command directly, providing the 54 | /// arguments to the scope 55 | fn execute(file: File, arguments: Vec, ui: &T, config_path: &str) -> ScriptResult 56 | where T: ApplicationUI, 57 | S: ScriptingEngine; 58 | 59 | /// Evaluate the contents of a file within the scripting runtime and execute 60 | /// the event trigger for getting autocompletion results, providing the 61 | /// arguments and prefix to the scope 62 | fn autocomplete(file: File, arguments: Vec, prefix: &str, ui: &T, config_path: &str) -> ScriptResult> 63 | where T: ApplicationUI, 64 | S: ScriptingEngine; 65 | 66 | /// Evaluate the contents of a file within the scripting runtime and execute 67 | /// the event trigger matching the BufferEvent, provided the window index, 68 | /// webview index, and requested URI to the scope. 69 | fn on_buffer_event(file: File, ui: &T, config_path: &str, window_index: u32, 70 | webview_index: u32, requested_uri: Option<&str>, 71 | event: &BufferEvent) -> ScriptResult<()> 72 | where T: ApplicationUI, 73 | S: ScriptingEngine; 74 | } 75 | -------------------------------------------------------------------------------- /webkitten-cocoa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webkitten-cocoa" 3 | version = "0.1.0" 4 | authors = ["Delisa Mason "] 5 | 6 | [dependencies] 7 | objc = "0.2.7" 8 | block = "0.1.6" 9 | lazy_static = "1.4.0" 10 | libc = "0.2.66" 11 | log = "0.4.8" 12 | webkitten = { path = "../" } 13 | macos = { path = "../macos" } 14 | dirs = "2.0.2" 15 | -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "webkitten@16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "webkitten@32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "webkitten@32-1.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "webkitten@64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "webkitten@128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "webkitten@256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "webkitten@256-1.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "webkitten@512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "webkitten@512-1.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "webkitten@1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@1024.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@128.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@16.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@256-1.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@256.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@32-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@32-1.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@32.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@512-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@512-1.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@512.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kattrali/webkitten/1c0b9ae2b21870eb2f1245f76de02a1a2a069918/webkitten-cocoa/app/Assets.xcassets/AppIcon.appiconset/webkitten@64.png -------------------------------------------------------------------------------- /webkitten-cocoa/app/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSApplicationCategoryType 26 | public.app-category.utilities 27 | LSMinimumSystemVersion 28 | $(MACOSX_DEPLOYMENT_TARGET) 29 | NSHumanReadableCopyright 30 | Copyright © 2016 Delisa Mason. All rights reserved. 31 | NSPrincipalClass 32 | NSApplication 33 | CFBundleURLTypes 34 | 35 | 36 | CFBundleURLName 37 | https URL 38 | CFBundleURLSchemes 39 | 40 | https 41 | 42 | 43 | 44 | CFBundleURLName 45 | http URL 46 | CFBundleURLSchemes 47 | 48 | http 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /webkitten-cocoa/app/main.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | 3 | func log(_ data: Data) { 4 | if let message = NSString(data: data, encoding: String.Encoding.utf8.rawValue) { 5 | NSLog("%@", message) 6 | } 7 | } 8 | 9 | let task = Process() 10 | let bundle = Bundle.main 11 | task.launchPath = bundle.path(forResource: "webkitten-cocoa", ofType: nil) 12 | task.environment = ["RUST_BACKTRACE": "1"] 13 | 14 | let stdOut = Pipe() 15 | let stdErr = Pipe() 16 | 17 | stdOut.fileHandleForReading.readabilityHandler = { log($0.availableData) } 18 | stdErr.fileHandleForReading.readabilityHandler = { log($0.availableData) } 19 | 20 | task.standardOutput = stdOut 21 | task.standardError = stdErr 22 | 23 | task.terminationHandler = { task in 24 | (task.standardOutput as AnyObject?)?.fileHandleForReading.readabilityHandler = nil 25 | (task.standardError as AnyObject?)?.fileHandleForReading.readabilityHandler = nil 26 | } 27 | 28 | task.launch() 29 | task.waitUntilExit() -------------------------------------------------------------------------------- /webkitten-cocoa/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | extern crate block; 4 | #[macro_use] 5 | extern crate lazy_static; 6 | extern crate libc; 7 | #[macro_use] 8 | extern crate log; 9 | #[macro_use] 10 | extern crate macos; 11 | #[macro_use] 12 | extern crate objc; 13 | extern crate webkitten; 14 | extern crate dirs; 15 | 16 | mod ui; 17 | mod runtime; 18 | 19 | use webkitten::ui::ApplicationUI; 20 | 21 | static SIMPLE_LOGGER: SimpleLogger = SimpleLogger; 22 | 23 | struct SimpleLogger; 24 | 25 | impl log::Log for SimpleLogger { 26 | fn enabled(&self, metadata: &log::Metadata) -> bool { 27 | metadata.level() <= log::Level::Info 28 | } 29 | 30 | fn log(&self, record: &log::Record) { 31 | if self.enabled(record.metadata()) { 32 | println!("{}:{}: {}", record.level(), 33 | record.module_path().unwrap(), record.args()); 34 | } 35 | } 36 | 37 | fn flush(&self) {} 38 | } 39 | 40 | fn main() { 41 | log::set_logger(&SIMPLE_LOGGER).unwrap(); 42 | log::set_max_level(log::LevelFilter::Info); 43 | runtime::declare_classes(); 44 | ui::UI.run(); 45 | } 46 | -------------------------------------------------------------------------------- /webkitten-cocoa/src/ui/application.rs: -------------------------------------------------------------------------------- 1 | use macos::ObjCClass; 2 | use macos::foundation::NSUInteger; 3 | use macos::appkit::{NSApplicationActivationPolicy,NSMenu,NSMenuItem,nsapp}; 4 | use webkitten::ui::BrowserConfiguration; 5 | 6 | use runtime::{KeyInputDelegate,AppDelegate}; 7 | 8 | 9 | pub fn initialize_app_env() -> AppDelegate { 10 | nsapp().set_activation_policy(NSApplicationActivationPolicy::Regular); 11 | let delegate = AppDelegate::new(); 12 | create_menu(&delegate); 13 | delegate 14 | } 15 | 16 | pub fn start_run_loop(delegate: &AppDelegate) { 17 | let app = nsapp(); 18 | app.activate(true); 19 | app.set_delegate(delegate); 20 | app.run(); 21 | } 22 | 23 | fn create_menu(delegate: &AppDelegate) { 24 | let menubar = NSMenu::new("").autorelease(); 25 | let app_menu_item = NSMenuItem::blank().autorelease(); 26 | app_menu_item.set_submenu(create_app_menu(delegate)); 27 | let edit_menu_item = NSMenuItem::blank().autorelease(); 28 | edit_menu_item.set_submenu(create_edit_menu()); 29 | let cmd_menu_item = NSMenuItem::blank().autorelease(); 30 | cmd_menu_item.set_submenu(create_command_menu()); 31 | let window_menu_item = NSMenuItem::blank().autorelease(); 32 | window_menu_item.set_submenu(create_window_menu()); 33 | menubar.add_item(app_menu_item); 34 | menubar.add_item(edit_menu_item); 35 | menubar.add_item(cmd_menu_item); 36 | menubar.add_item(window_menu_item); 37 | nsapp().set_main_menu(menubar); 38 | } 39 | 40 | fn create_window_menu() -> NSMenu { 41 | let menu = NSMenu::new("Window").autorelease(); 42 | nsapp().set_windows_menu(&menu); 43 | menu 44 | } 45 | 46 | fn create_app_menu(delegate: &AppDelegate) -> NSMenu { 47 | let app_menu = NSMenu::new("").autorelease(); 48 | app_menu.set_autoenables_items(false); 49 | let default_item = NSMenuItem::new("Set as default Web Browser", sel!(setAsDefaultBrowser), "").autorelease(); 50 | default_item.set_target(delegate); 51 | app_menu.add_item(default_item); 52 | let quit_item = NSMenuItem::new("Quit", sel!(terminate:), "q").autorelease(); 53 | app_menu.add_item(quit_item); 54 | app_menu 55 | } 56 | 57 | fn create_edit_menu() -> NSMenu { 58 | let edit_menu = NSMenu::new("Edit").autorelease(); 59 | edit_menu.add_item(NSMenuItem::new("Undo", sel!(undo:), "z").autorelease()); 60 | edit_menu.add_item(NSMenuItem::new("Redo", sel!(redo:), "Z").autorelease()); 61 | edit_menu.add_item(NSMenuItem::separator()); 62 | edit_menu.add_item(NSMenuItem::new("Cut", sel!(cut:), "x").autorelease()); 63 | edit_menu.add_item(NSMenuItem::new("Copy", sel!(copy:), "c").autorelease()); 64 | edit_menu.add_item(NSMenuItem::new("Paste", sel!(paste:), "v").autorelease()); 65 | edit_menu.add_item(NSMenuItem::separator()); 66 | edit_menu.add_item(NSMenuItem::new("Select All", sel!(selectAll:), "a").autorelease()); 67 | edit_menu 68 | } 69 | 70 | fn create_command_menu() -> NSMenu { 71 | let cmd_menu = NSMenu::new("Command").autorelease(); 72 | for (command_name, (keychar, modifier)) in super::UI.engine.config.command_keybindings() { 73 | let mut key = String::new(); 74 | key.push(keychar); 75 | let item = NSMenuItem::new(&command_name, sel!(runKeybindingCommand),&key).autorelease(); 76 | item.set_key_equivalent_modifier_mask(modifier as NSUInteger); 77 | item.set_target(&KeyInputDelegate::new(&command_name)); 78 | cmd_menu.add_item(item); 79 | } 80 | cmd_menu 81 | } 82 | -------------------------------------------------------------------------------- /webkitten-cocoa/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod application; 2 | pub mod window; 3 | 4 | use std::fs::File; 5 | use std::io::Read; 6 | use std::marker::PhantomData; 7 | use std::process; 8 | 9 | use webkitten::ui::*; 10 | use webkitten::config::Config; 11 | use webkitten::Engine; 12 | use webkitten::script::{ScriptingEngine,LuaEngine}; 13 | use webkitten::optparse::parse_opts; 14 | use macos::foundation::{NSURLRequest,NSURL,NSString,NSAutoreleasePool}; 15 | use macos::appkit::{NSPasteboard,nsapp}; 16 | use macos::webkit::*; 17 | use macos::{Id,nil}; 18 | use block::ConcreteBlock; 19 | 20 | use runtime::log_error_description; 21 | 22 | 23 | const DEFAULT_CONFIG_PATH: &'static str = ".config/webkitten/config.toml"; 24 | 25 | lazy_static! { 26 | pub static ref UI: CocoaUI = { 27 | if let Some(home_dir) = dirs::home_dir() { 28 | let default_config_path = &format!("{}/{}", home_dir.display(), DEFAULT_CONFIG_PATH); 29 | let run_config = parse_opts(default_config_path); 30 | if let Some((status, message)) = run_config.exit_status { 31 | print!("{}", message); 32 | process::exit(status); 33 | } 34 | Engine::new(run_config) 35 | .and_then(|engine| CocoaUI::new(engine)) 36 | .unwrap_or_else(|| panic!("Unable to initialize application")) 37 | } else { 38 | panic!("Unable to locate home directory"); 39 | } 40 | }; 41 | } 42 | 43 | 44 | pub struct CocoaUI { 45 | pub engine: Engine, 46 | engine_type: PhantomData 47 | } 48 | 49 | impl CocoaUI { 50 | 51 | fn compile_content_extensions(&self, completion: F) 52 | where F: Fn(bool) + 'static { 53 | let filter_path = self.engine.config.content_filter_path(); 54 | if let Some(mut file) = filter_path.and_then(|p| File::open(p).ok()) { 55 | let mut contents = String::new(); 56 | if let Some(_) = file.read_to_string(&mut contents).ok() { 57 | let block = ConcreteBlock::new(move |_: Id, err: Id| { 58 | log_error_description(err); 59 | completion(err == nil); 60 | }); 61 | let store = _WKUserContentExtensionStore::default_store(); 62 | store.compile_content_extension("filter", &contents, &block.copy()); 63 | } 64 | } 65 | } 66 | 67 | fn open_first_window(&self) { 68 | if !self.engine.initial_pages().is_empty() { 69 | for page in self.engine.initial_pages() { 70 | self.open_window::<_, Config>(Some(page.as_str()), None); 71 | } 72 | } else { 73 | self.open_window::<_, Config>(self.engine.config.start_page(), None); 74 | } 75 | } 76 | } 77 | 78 | impl ApplicationUI for CocoaUI { 79 | 80 | fn new(engine: Engine) -> Option { 81 | Some(CocoaUI { engine: engine, engine_type: PhantomData }) 82 | } 83 | 84 | fn run(&self) { 85 | let pool = NSAutoreleasePool::new(); 86 | self.compile_content_extensions(|_| {}); 87 | let delegate = application::initialize_app_env(); 88 | self.open_first_window(); 89 | application::start_run_loop(&delegate); 90 | pool.drain(); 91 | } 92 | 93 | fn copy(&self, text: &str) { 94 | NSPasteboard::general().copy(text); 95 | } 96 | 97 | fn execute_command(&self, window_index: Option, text: &str) { 98 | UI.engine.execute_command::, _>(&UI, window_index, text); 99 | } 100 | 101 | fn open_window(&self, uri: Option, config: Option) -> u32 102 | where U: Into, 103 | B: BrowserConfiguration { 104 | if let Some(uri) = uri { 105 | window::open(Some(uri), config) 106 | } else { 107 | window::open(self.engine.config.start_page(), config) 108 | } 109 | } 110 | 111 | fn close_window(&self, index: u32) { 112 | window::close(index); 113 | } 114 | 115 | fn focused_window_index(&self) -> Option { 116 | window::focused_index() 117 | } 118 | 119 | fn focus_window(&self, index: u32) { 120 | window::focus(index); 121 | } 122 | 123 | fn focus_window_area(&self, index: u32, area: WindowArea) { 124 | window::focus_area(index, area); 125 | } 126 | 127 | fn window_count(&self) -> u32 { 128 | nsapp().ordered_windows().count() as u32 129 | } 130 | 131 | fn toggle_window(&self, window_index: u32, visible: bool) { 132 | window::toggle(window_index, visible); 133 | } 134 | 135 | fn resize_window(&self, window_index: u32, width: u32, height: u32) { 136 | window::resize(window_index, width, height); 137 | } 138 | 139 | fn command_field_text(&self, window_index: u32) -> String { 140 | window::command_field_text(window_index) 141 | } 142 | 143 | fn command_field_visible(&self, window_index: u32) -> bool { 144 | window::command_field_visible(window_index) 145 | } 146 | 147 | fn set_command_field_text(&self, window_index: u32, text: &str) { 148 | window::set_command_field_text(window_index, text); 149 | } 150 | 151 | fn set_command_field_visible(&self, window_index: u32, visible: bool) { 152 | window::set_command_field_visible(window_index, visible); 153 | } 154 | 155 | fn window_title(&self, window_index: u32) -> String { 156 | window::title(window_index) 157 | } 158 | 159 | fn set_window_title(&self, window_index: u32, title: &str) { 160 | window::set_title(window_index, title); 161 | } 162 | 163 | fn focused_webview_index(&self, window_index: u32) -> Option { 164 | window::focused_webview_index(window_index) 165 | } 166 | 167 | fn webview_count(&self, window_index: u32) -> u32 { 168 | window::webview_count(window_index) 169 | } 170 | 171 | fn open_webview(&self, window_index: u32, uri: Option, config: Option) 172 | where U: Into, 173 | B: BrowserConfiguration { 174 | if let Some(uri) = uri { 175 | window::open_webview(window_index, Some(uri), config); 176 | } else { 177 | window::open_webview(window_index, self.engine.config.start_page(), config); 178 | } 179 | } 180 | 181 | fn close_webview(&self, window_index: u32, webview_index: u32) { 182 | window::close_webview(window_index, webview_index); 183 | } 184 | 185 | fn focus_webview(&self, window_index: u32, webview_index: u32) { 186 | window::focus_webview(window_index, webview_index); 187 | } 188 | 189 | fn reload_webview(&self, window_index: u32, webview_index: u32, disable_filters: bool) { 190 | if let Some(webview) = window::webview(window_index, webview_index) { 191 | match disable_filters { 192 | true => webview.reload_without_content_blockers(), 193 | false => webview.reload() 194 | } 195 | } 196 | } 197 | 198 | fn set_uri(&self, window_index: u32, webview_index: u32, uri: &str) { 199 | if let Some(webview) = window::webview(window_index, webview_index) { 200 | webview.load_request(create_request(uri)); 201 | } 202 | } 203 | 204 | fn go_back(&self, window_index: u32, webview_index: u32) -> bool { 205 | if let Some(webview) = window::webview(window_index, webview_index) { 206 | if webview.can_go_back() { 207 | webview.go_back(); 208 | return true; 209 | } 210 | } 211 | false 212 | } 213 | 214 | fn go_forward(&self, window_index: u32, webview_index: u32) -> bool { 215 | if let Some(webview) = window::webview(window_index, webview_index) { 216 | if webview.can_go_forward() { 217 | webview.go_forward(); 218 | return true; 219 | } 220 | } 221 | false 222 | } 223 | 224 | fn uri(&self, window_index: u32, webview_index: u32) -> String { 225 | String::from(window::webview(window_index, webview_index) 226 | .and_then(|webview| webview.url()) 227 | .and_then(|u| u.absolute_string().as_str()) 228 | .unwrap_or("")) 229 | } 230 | 231 | fn webview_title(&self, window_index: u32, webview_index: u32) -> String { 232 | String::from(window::webview(window_index, webview_index) 233 | .and_then(|webview| webview.title()) 234 | .and_then(|title| title.as_str()) 235 | .unwrap_or("")) 236 | } 237 | 238 | fn find_string(&self, window_index: u32, webview_index: u32, query: &str) { 239 | if let Some(webview) = window::webview(window_index, webview_index) { 240 | webview.find_string(query) 241 | } 242 | } 243 | 244 | fn hide_find_results(&self, window_index: u32, webview_index: u32) { 245 | if let Some(webview) = window::webview(window_index, webview_index) { 246 | webview.hide_find_results() 247 | } 248 | } 249 | 250 | fn run_javascript(&self, window_index: u32, webview_index: u32, script: &str) { 251 | if let Some(webview) = window::webview(window_index, webview_index) { 252 | webview.evaluate_javascript(script) 253 | } 254 | } 255 | 256 | fn apply_styles(&self, window_index: u32, webview_index: u32, styles: &str) { 257 | if let Some(webview) = window::webview(window_index, webview_index) { 258 | let controller = webview.configuration().user_content_controller(); 259 | if controller.can_add_user_style_sheet() { 260 | let sheet = _WKUserStyleSheet::new(styles); 261 | controller.add_user_style_sheet(sheet); 262 | } else { 263 | info!("Using fallback stylesheet"); 264 | let formatted_style = styles.replace("\"", "\\\"").replace("\n", ""); 265 | let script = format!(r#" 266 | var head = document.getElementsByTagName('head')[0], 267 | style = document.createElement('style'), 268 | content = document.createTextNode('{}'); 269 | style.appendChild(content); 270 | if (head != undefined) {{ 271 | head.appendChild(style); 272 | }}"#, formatted_style); 273 | webview.evaluate_javascript(&script); 274 | } 275 | } 276 | } 277 | } 278 | 279 | pub fn create_request(uri: &str) -> NSURLRequest { 280 | let mut target = String::from(uri); 281 | if !target.contains("://") { 282 | target = format!("http://{}", target); 283 | } 284 | NSURLRequest::from(NSURL::from(NSString::from(&target))) 285 | } 286 | -------------------------------------------------------------------------------- /webkitten-cocoa/webkitten.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8A7514451D3DDD42001C4112 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8A7514441D3DDD42001C4112 /* Assets.xcassets */; }; 11 | 8A7514521D3DE066001C4112 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A7514511D3DE066001C4112 /* main.swift */; }; 12 | 8AEA2C071D3E011100A82429 /* webkitten-cocoa in Resources */ = {isa = PBXBuildFile; fileRef = 8AEA2C061D3E011100A82429 /* webkitten-cocoa */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXContainerItemProxy section */ 16 | 8A41C8F51D487CF000515E73 /* PBXContainerItemProxy */ = { 17 | isa = PBXContainerItemProxy; 18 | containerPortal = 8A7514331D3DDD42001C4112 /* Project object */; 19 | proxyType = 1; 20 | remoteGlobalIDString = 8A41C8F11D487CDE00515E73; 21 | remoteInfo = "webkitten-cocoa"; 22 | }; 23 | /* End PBXContainerItemProxy section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | 8A75143B1D3DDD42001C4112 /* Webkitten.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Webkitten.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | 8A7514441D3DDD42001C4112 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 28 | 8A7514491D3DDD42001C4112 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 29 | 8A7514511D3DE066001C4112 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 30 | 8AEA2C061D3E011100A82429 /* webkitten-cocoa */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.executable"; name = "webkitten-cocoa"; path = "target/release/webkitten-cocoa"; sourceTree = SOURCE_ROOT; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 8A7514381D3DDD42001C4112 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 8A7514321D3DDD42001C4112 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 8A75143D1D3DDD42001C4112 /* app */, 48 | 8A75143C1D3DDD42001C4112 /* Products */, 49 | ); 50 | sourceTree = ""; 51 | }; 52 | 8A75143C1D3DDD42001C4112 /* Products */ = { 53 | isa = PBXGroup; 54 | children = ( 55 | 8A75143B1D3DDD42001C4112 /* Webkitten.app */, 56 | ); 57 | name = Products; 58 | sourceTree = ""; 59 | }; 60 | 8A75143D1D3DDD42001C4112 /* app */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | 8A7514511D3DE066001C4112 /* main.swift */, 64 | 8AEA2C061D3E011100A82429 /* webkitten-cocoa */, 65 | 8A7514441D3DDD42001C4112 /* Assets.xcassets */, 66 | 8A7514491D3DDD42001C4112 /* Info.plist */, 67 | ); 68 | path = app; 69 | sourceTree = ""; 70 | }; 71 | /* End PBXGroup section */ 72 | 73 | /* Begin PBXLegacyTarget section */ 74 | 8A41C8F11D487CDE00515E73 /* webkitten-cocoa */ = { 75 | isa = PBXLegacyTarget; 76 | buildArgumentsString = release; 77 | buildConfigurationList = 8A41C8F41D487CDE00515E73 /* Build configuration list for PBXLegacyTarget "webkitten-cocoa" */; 78 | buildPhases = ( 79 | ); 80 | buildToolPath = /usr/bin/make; 81 | buildWorkingDirectory = ..; 82 | dependencies = ( 83 | ); 84 | name = "webkitten-cocoa"; 85 | passBuildSettingsInEnvironment = 1; 86 | productName = "webkitten-cocoa"; 87 | }; 88 | /* End PBXLegacyTarget section */ 89 | 90 | /* Begin PBXNativeTarget section */ 91 | 8A75143A1D3DDD42001C4112 /* Webkitten */ = { 92 | isa = PBXNativeTarget; 93 | buildConfigurationList = 8A75144C1D3DDD42001C4112 /* Build configuration list for PBXNativeTarget "Webkitten" */; 94 | buildPhases = ( 95 | 8A7514371D3DDD42001C4112 /* Sources */, 96 | 8A7514381D3DDD42001C4112 /* Frameworks */, 97 | 8A7514391D3DDD42001C4112 /* Resources */, 98 | ); 99 | buildRules = ( 100 | ); 101 | dependencies = ( 102 | 8A41C8F61D487CF000515E73 /* PBXTargetDependency */, 103 | ); 104 | name = Webkitten; 105 | productName = Webkitten; 106 | productReference = 8A75143B1D3DDD42001C4112 /* Webkitten.app */; 107 | productType = "com.apple.product-type.application"; 108 | }; 109 | /* End PBXNativeTarget section */ 110 | 111 | /* Begin PBXProject section */ 112 | 8A7514331D3DDD42001C4112 /* Project object */ = { 113 | isa = PBXProject; 114 | attributes = { 115 | LastUpgradeCheck = 0730; 116 | ORGANIZATIONNAME = "Delisa Mason"; 117 | TargetAttributes = { 118 | 8A41C8F11D487CDE00515E73 = { 119 | CreatedOnToolsVersion = 7.3.1; 120 | }; 121 | 8A75143A1D3DDD42001C4112 = { 122 | CreatedOnToolsVersion = 7.3.1; 123 | LastSwiftMigration = 0800; 124 | }; 125 | }; 126 | }; 127 | buildConfigurationList = 8A7514361D3DDD42001C4112 /* Build configuration list for PBXProject "webkitten" */; 128 | compatibilityVersion = "Xcode 3.2"; 129 | developmentRegion = English; 130 | hasScannedForEncodings = 0; 131 | knownRegions = ( 132 | en, 133 | Base, 134 | ); 135 | mainGroup = 8A7514321D3DDD42001C4112; 136 | productRefGroup = 8A75143C1D3DDD42001C4112 /* Products */; 137 | projectDirPath = ""; 138 | projectRoot = ""; 139 | targets = ( 140 | 8A75143A1D3DDD42001C4112 /* Webkitten */, 141 | 8A41C8F11D487CDE00515E73 /* webkitten-cocoa */, 142 | ); 143 | }; 144 | /* End PBXProject section */ 145 | 146 | /* Begin PBXResourcesBuildPhase section */ 147 | 8A7514391D3DDD42001C4112 /* Resources */ = { 148 | isa = PBXResourcesBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 8AEA2C071D3E011100A82429 /* webkitten-cocoa in Resources */, 152 | 8A7514451D3DDD42001C4112 /* Assets.xcassets in Resources */, 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | }; 156 | /* End PBXResourcesBuildPhase section */ 157 | 158 | /* Begin PBXSourcesBuildPhase section */ 159 | 8A7514371D3DDD42001C4112 /* Sources */ = { 160 | isa = PBXSourcesBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 8A7514521D3DE066001C4112 /* main.swift in Sources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin PBXTargetDependency section */ 170 | 8A41C8F61D487CF000515E73 /* PBXTargetDependency */ = { 171 | isa = PBXTargetDependency; 172 | target = 8A41C8F11D487CDE00515E73 /* webkitten-cocoa */; 173 | targetProxy = 8A41C8F51D487CF000515E73 /* PBXContainerItemProxy */; 174 | }; 175 | /* End PBXTargetDependency section */ 176 | 177 | /* Begin XCBuildConfiguration section */ 178 | 8A41C8F21D487CDE00515E73 /* Debug */ = { 179 | isa = XCBuildConfiguration; 180 | buildSettings = { 181 | DEBUGGING_SYMBOLS = YES; 182 | DEBUG_INFORMATION_FORMAT = dwarf; 183 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES; 184 | GCC_OPTIMIZATION_LEVEL = 0; 185 | OTHER_CFLAGS = ""; 186 | OTHER_LDFLAGS = ""; 187 | PRODUCT_NAME = "$(TARGET_NAME)"; 188 | }; 189 | name = Debug; 190 | }; 191 | 8A41C8F31D487CDE00515E73 /* Release */ = { 192 | isa = XCBuildConfiguration; 193 | buildSettings = { 194 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 195 | OTHER_CFLAGS = ""; 196 | OTHER_LDFLAGS = ""; 197 | PRODUCT_NAME = "$(TARGET_NAME)"; 198 | }; 199 | name = Release; 200 | }; 201 | 8A75144A1D3DDD42001C4112 /* Debug */ = { 202 | isa = XCBuildConfiguration; 203 | buildSettings = { 204 | ALWAYS_SEARCH_USER_PATHS = NO; 205 | CLANG_ANALYZER_NONNULL = YES; 206 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 207 | CLANG_CXX_LIBRARY = "libc++"; 208 | CLANG_ENABLE_MODULES = YES; 209 | CLANG_ENABLE_OBJC_ARC = YES; 210 | CLANG_WARN_BOOL_CONVERSION = YES; 211 | CLANG_WARN_CONSTANT_CONVERSION = YES; 212 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 213 | CLANG_WARN_EMPTY_BODY = YES; 214 | CLANG_WARN_ENUM_CONVERSION = YES; 215 | CLANG_WARN_INT_CONVERSION = YES; 216 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 217 | CLANG_WARN_UNREACHABLE_CODE = YES; 218 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 219 | CODE_SIGN_IDENTITY = "-"; 220 | COPY_PHASE_STRIP = NO; 221 | DEBUG_INFORMATION_FORMAT = dwarf; 222 | ENABLE_STRICT_OBJC_MSGSEND = YES; 223 | ENABLE_TESTABILITY = YES; 224 | GCC_C_LANGUAGE_STANDARD = gnu99; 225 | GCC_DYNAMIC_NO_PIC = NO; 226 | GCC_NO_COMMON_BLOCKS = YES; 227 | GCC_OPTIMIZATION_LEVEL = 0; 228 | GCC_PREPROCESSOR_DEFINITIONS = ( 229 | "DEBUG=1", 230 | "$(inherited)", 231 | ); 232 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 233 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 234 | GCC_WARN_UNDECLARED_SELECTOR = YES; 235 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 236 | GCC_WARN_UNUSED_FUNCTION = YES; 237 | GCC_WARN_UNUSED_VARIABLE = YES; 238 | MACOSX_DEPLOYMENT_TARGET = 10.11; 239 | MTL_ENABLE_DEBUG_INFO = YES; 240 | ONLY_ACTIVE_ARCH = YES; 241 | SDKROOT = macosx; 242 | }; 243 | name = Debug; 244 | }; 245 | 8A75144B1D3DDD42001C4112 /* Release */ = { 246 | isa = XCBuildConfiguration; 247 | buildSettings = { 248 | ALWAYS_SEARCH_USER_PATHS = NO; 249 | CLANG_ANALYZER_NONNULL = YES; 250 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 251 | CLANG_CXX_LIBRARY = "libc++"; 252 | CLANG_ENABLE_MODULES = YES; 253 | CLANG_ENABLE_OBJC_ARC = YES; 254 | CLANG_WARN_BOOL_CONVERSION = YES; 255 | CLANG_WARN_CONSTANT_CONVERSION = YES; 256 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 257 | CLANG_WARN_EMPTY_BODY = YES; 258 | CLANG_WARN_ENUM_CONVERSION = YES; 259 | CLANG_WARN_INT_CONVERSION = YES; 260 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 261 | CLANG_WARN_UNREACHABLE_CODE = YES; 262 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 263 | CODE_SIGN_IDENTITY = "-"; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu99; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | MACOSX_DEPLOYMENT_TARGET = 10.11; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | SDKROOT = macosx; 279 | }; 280 | name = Release; 281 | }; 282 | 8A75144D1D3DDD42001C4112 /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 286 | CLANG_ENABLE_MODULES = YES; 287 | COMBINE_HIDPI_IMAGES = YES; 288 | INFOPLIST_FILE = app/Info.plist; 289 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 290 | PRODUCT_BUNDLE_IDENTIFIER = me.delisa.Webkitten; 291 | PRODUCT_NAME = "$(TARGET_NAME)"; 292 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 293 | SWIFT_VERSION = 3.0; 294 | }; 295 | name = Debug; 296 | }; 297 | 8A75144E1D3DDD42001C4112 /* Release */ = { 298 | isa = XCBuildConfiguration; 299 | buildSettings = { 300 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 301 | CLANG_ENABLE_MODULES = YES; 302 | COMBINE_HIDPI_IMAGES = YES; 303 | INFOPLIST_FILE = app/Info.plist; 304 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks"; 305 | PRODUCT_BUNDLE_IDENTIFIER = me.delisa.Webkitten; 306 | PRODUCT_NAME = "$(TARGET_NAME)"; 307 | SWIFT_VERSION = 3.0; 308 | }; 309 | name = Release; 310 | }; 311 | /* End XCBuildConfiguration section */ 312 | 313 | /* Begin XCConfigurationList section */ 314 | 8A41C8F41D487CDE00515E73 /* Build configuration list for PBXLegacyTarget "webkitten-cocoa" */ = { 315 | isa = XCConfigurationList; 316 | buildConfigurations = ( 317 | 8A41C8F21D487CDE00515E73 /* Debug */, 318 | 8A41C8F31D487CDE00515E73 /* Release */, 319 | ); 320 | defaultConfigurationIsVisible = 0; 321 | defaultConfigurationName = Release; 322 | }; 323 | 8A7514361D3DDD42001C4112 /* Build configuration list for PBXProject "webkitten" */ = { 324 | isa = XCConfigurationList; 325 | buildConfigurations = ( 326 | 8A75144A1D3DDD42001C4112 /* Debug */, 327 | 8A75144B1D3DDD42001C4112 /* Release */, 328 | ); 329 | defaultConfigurationIsVisible = 0; 330 | defaultConfigurationName = Release; 331 | }; 332 | 8A75144C1D3DDD42001C4112 /* Build configuration list for PBXNativeTarget "Webkitten" */ = { 333 | isa = XCConfigurationList; 334 | buildConfigurations = ( 335 | 8A75144D1D3DDD42001C4112 /* Debug */, 336 | 8A75144E1D3DDD42001C4112 /* Release */, 337 | ); 338 | defaultConfigurationIsVisible = 0; 339 | defaultConfigurationName = Release; 340 | }; 341 | /* End XCConfigurationList section */ 342 | }; 343 | rootObject = 8A7514331D3DDD42001C4112 /* Project object */; 344 | } 345 | -------------------------------------------------------------------------------- /webkitten-cocoa/webkitten.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /webkitten-cocoa/webkitten.xcodeproj/xcshareddata/xcschemes/Webkitten.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /webkitten-gtk/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "webkitten-gtk" 3 | version = "0.1.0" 4 | authors = ["Delisa Mason "] 5 | build = "build.rs" 6 | 7 | [dependencies] 8 | libc = "0.2.8" 9 | gtk-sys = "0.2.1" 10 | gobject-sys = "0.2.1" 11 | glib-sys = "0.2.1" 12 | -------------------------------------------------------------------------------- /webkitten-gtk/src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate webkitten; 2 | extern crate getopts; 3 | 4 | use std::{env,fs}; 5 | use std::io::Write; 6 | use getopts::Options; 7 | use webkitten::Application; 8 | 9 | /// Print command usage, given invocation path and options 10 | fn print_usage(program: &str, opts: Options) { 11 | let brief = format!("Usage: {} FILE [options]", program); 12 | print!("{}", opts.usage(&brief)); 13 | } 14 | 15 | /// Check if the given config path exists, creating if not required to already 16 | /// exist 17 | fn validate_config_path(config_path: String, require_exists: bool) { 18 | if !fs::metadata(config_path.as_str()).is_ok() { 19 | if require_exists { 20 | panic!("No config found at path: {}", config_path); 21 | } 22 | write_default_config(config_path); 23 | } 24 | } 25 | 26 | fn write_default_config(config_path: String) { 27 | match fs::File::create(config_path.as_str()) { 28 | Ok(ref mut file) => { 29 | if file.write(webkitten::config::DEFAULT_CONFIG.as_bytes()).is_ok() { 30 | let result = file.flush(); 31 | if result.is_err() { 32 | panic!("Unable to create default config ({}): {}", 33 | config_path, 34 | result.err().unwrap()); 35 | } 36 | } 37 | }, 38 | Err(e) => panic!("Unable to create default config ({}): {}", config_path, e) 39 | } 40 | } 41 | 42 | /// Load a new instance of `webkitten::Application` with a given config path 43 | fn load_app(config_path: String, require_exists: bool) { 44 | validate_config_path(config_path.clone(), require_exists); 45 | match Application::new(config_path.as_str()) { 46 | Some(ref mut app) => app.run(), 47 | None => panic!("Unable to parse config from path: {}", config_path) 48 | } 49 | } 50 | 51 | /// Computes default configuration path 52 | fn default_config_path() -> String { 53 | match env::var("HOME") { 54 | Ok(home) => format!("{}/.config/webkitten/config.toml", home), 55 | Err(_) => panic!("Unable to load default config from HOME") 56 | } 57 | } 58 | 59 | fn main() { 60 | let args: Vec = env::args().collect(); 61 | let mut opts = Options::new(); 62 | let program = args[0].clone(); 63 | opts.optopt("c", "config", "Set the configuration path", "PATH"); 64 | opts.optflag("h", "help", "Print this help text"); 65 | let matches = match opts.parse(&args[1..]) { 66 | Ok(m) => { m } 67 | Err(f) => { panic!(f.to_string()) } 68 | }; 69 | if matches.opt_present("h") { 70 | print_usage(&program, opts); 71 | return; 72 | } 73 | match matches.opt_str("c") { 74 | Some(config_path) => load_app(config_path, true), 75 | None => load_app(default_config_path(), false) 76 | } 77 | } 78 | 79 | fn with_gtk_app(callback: F) where F: FnOnce() -> () { 80 | unsafe { gtk_sys::gtk_init(0 as *mut c_int, 0 as *mut *mut *mut c_char); } 81 | callback(); 82 | unsafe { gtk_sys::gtk_main(); } 83 | } 84 | 85 | pub fn run(args: Vec<&str>) where T: ui::ApplicationUI { 86 | 87 | } 88 | -------------------------------------------------------------------------------- /webkitten-gtk/src/ui/box_container.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | use super::{FlowDirection,GtkWidgetConvertible,GtkContainerConvertible,gboolean}; 3 | use super::gtk_sys::{GtkBox,GtkWidget,GtkContainer,GtkOrientation}; 4 | use super::gtk_sys::{ 5 | gtk_box_new, 6 | gtk_box_pack_end, 7 | gtk_container_remove}; 8 | 9 | pub struct BoxContainer { 10 | gtk_widget: *mut GtkWidget, 11 | } 12 | 13 | impl BoxContainer { 14 | 15 | pub fn new(direction: FlowDirection) -> Self { 16 | let orientation = gtk_orientation(direction); 17 | BoxContainer { gtk_widget: create_gtk_widget(orientation) } 18 | } 19 | 20 | /// Adds a child widget to the container, bottom to top 21 | /// 22 | /// ## Panics 23 | /// 24 | /// Panics if the widget is already a child of the container 25 | pub fn add_child(&mut self, child: *mut GtkWidget, fill: bool) { 26 | let gfill = gboolean(fill); 27 | unsafe { gtk_box_pack_end(self.gtk_box(), child, gfill, gfill, 0); } 28 | } 29 | 30 | /// Removes a child widget from the container 31 | pub fn remove_child(&mut self, child: *mut GtkWidget) { 32 | unsafe { gtk_container_remove(self.gtk_container(), child); } 33 | } 34 | 35 | fn gtk_box(&self) -> *mut GtkBox { 36 | return unsafe { transmute(self.gtk_widget) }; 37 | } 38 | } 39 | 40 | impl GtkContainerConvertible for BoxContainer { 41 | 42 | fn gtk_container(&self) -> *mut GtkContainer { 43 | return unsafe { transmute(self.gtk_widget) } 44 | } 45 | } 46 | 47 | impl GtkWidgetConvertible for BoxContainer { 48 | 49 | fn gtk_widget(&self) -> *mut GtkWidget { 50 | self.gtk_widget 51 | } 52 | } 53 | 54 | fn create_gtk_widget(direction: GtkOrientation) -> *mut GtkWidget { 55 | return unsafe { gtk_box_new(direction, 0) }; 56 | } 57 | 58 | fn gtk_orientation(direction: FlowDirection) -> GtkOrientation { 59 | return match direction { 60 | FlowDirection::Vertical => GtkOrientation::Vertical, 61 | FlowDirection::Horizontal => GtkOrientation::Horizontal, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /webkitten-gtk/src/ui/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate gtk_sys; 2 | extern crate gobject_sys; 3 | extern crate glib_sys; 4 | extern crate libc; 5 | 6 | use gobject_sys::GObject; 7 | use gtk_sys::{GtkWidget,GtkEditable,GtkContainer}; 8 | use std::ffi::CString; 9 | use std::mem; 10 | 11 | mod text_field; 12 | mod box_container; 13 | mod window; 14 | mod web_view; 15 | 16 | pub use self::text_field::TextField; 17 | pub use self::box_container::BoxContainer; 18 | pub use self::window::Window; 19 | pub use self::web_view::WebView; 20 | 21 | pub type GtkWidgetCallback = unsafe extern "C" fn(*mut gtk_sys::GtkWidget, 22 | *mut gtk_sys::GtkWidget); 23 | 24 | /// Structures with a GObject representation 25 | trait GObjectConvertible { 26 | fn gobject(&self) -> *mut GObject; 27 | } 28 | 29 | /// Structures with a GTK+ widget representation 30 | trait GtkContainerConvertible { 31 | fn gtk_container(&self) -> *mut GtkContainer; 32 | } 33 | 34 | /// Structures with a GTK+ widget representation 35 | trait GtkWidgetConvertible { 36 | fn gtk_widget(&self) -> *mut GtkWidget; 37 | } 38 | 39 | /// Structures with a GTK+ editable representation 40 | trait GtkEditableConvertible { 41 | fn gtk_editable(&self) -> *mut GtkEditable; 42 | } 43 | 44 | pub enum FlowDirection { 45 | Vertical, 46 | Horizontal 47 | } 48 | 49 | // Add a callback to an event on a GTK+ widget 50 | pub fn add_widget_callback(widget: *mut GtkWidget, 51 | signal: &'static str, 52 | callback: GtkWidgetCallback) { 53 | unsafe { 54 | let gobject_widget: *mut gobject_sys::GObject = mem::transmute(widget); 55 | let signal_ptr = CString::new(signal).unwrap().as_ptr(); 56 | let mut data = 0; 57 | let gcallback: gobject_sys::GCallback = mem::transmute(Some(callback)); 58 | gobject_sys::g_signal_connect_data(gobject_widget, signal_ptr, gcallback, 59 | &mut data as *mut _ as glib_sys::gpointer, 60 | None, gobject_sys::GConnectFlags::empty()); 61 | } 62 | } 63 | 64 | fn gboolean(value: bool) -> libc::c_int { 65 | return if value { 1 } else { 0 } 66 | } 67 | -------------------------------------------------------------------------------- /webkitten-gtk/src/ui/text_field.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | use std::ffi::CString; 3 | use super::gtk_sys::{GtkEntry,GtkEditable,GtkWidget}; 4 | use super::gtk_sys::{ 5 | gtk_entry_get_text, 6 | gtk_entry_set_text, 7 | gtk_entry_new, 8 | gtk_entry_set_has_frame}; 9 | 10 | pub struct TextField { 11 | gtk_widget: *mut GtkWidget, 12 | } 13 | 14 | impl TextField { 15 | 16 | pub fn new() -> Self { 17 | TextField { gtk_widget: create_gtk_widget() } 18 | } 19 | 20 | /// The displayed text 21 | pub fn text(&self) -> String { 22 | return unsafe { 23 | let mut raw_text = *gtk_entry_get_text(self.gtk_entry()); 24 | let c_text = CString::from_raw(&mut raw_text); 25 | return match c_text.into_string() { 26 | Ok(text) => text, 27 | _ => panic!("Unable to parse text") 28 | } 29 | } 30 | } 31 | 32 | /// Changes the displayed text 33 | pub fn set_text(&mut self, text: &'static str) { 34 | match CString::new(text) { 35 | Ok(entry_text) => { unsafe { 36 | gtk_entry_set_text(self.gtk_entry(), entry_text.into_raw()) 37 | }}, 38 | _ => {} 39 | } 40 | } 41 | 42 | fn gtk_entry(&self) -> *mut GtkEntry { 43 | return unsafe { transmute(self.gtk_widget) }; 44 | } 45 | } 46 | 47 | impl super::GtkWidgetConvertible for TextField { 48 | 49 | fn gtk_widget(&self) -> *mut GtkWidget { 50 | self.gtk_widget 51 | } 52 | } 53 | 54 | impl super::GtkEditableConvertible for TextField { 55 | 56 | fn gtk_editable(&self) -> *mut GtkEditable { 57 | return unsafe { transmute(self.gtk_widget) } 58 | } 59 | } 60 | 61 | fn create_gtk_widget() -> *mut GtkWidget { 62 | return unsafe { 63 | let widget = gtk_entry_new(); 64 | let entry: *mut GtkEntry = transmute(widget); 65 | gtk_entry_set_has_frame(entry, super::gboolean(false)); 66 | widget 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /webkitten-gtk/src/ui/web_view.rs: -------------------------------------------------------------------------------- 1 | use super::gtk_sys; 2 | use super::gobject_sys::{GObject,g_object_ref,g_object_unref}; 3 | use super::gtk_sys::GtkWidget; 4 | use super::super::webkitgtk_sys::*; 5 | use super::{GtkWidgetConvertible,GObjectConvertible}; 6 | use std::ffi::CString; 7 | use std::mem::transmute; 8 | use std::ops::Drop; 9 | 10 | pub struct WebView { 11 | gtk_widget: *mut GtkWidget, 12 | } 13 | 14 | impl WebView { 15 | 16 | pub fn new() -> Self { 17 | WebView { gtk_widget: create_gtk_widget() } 18 | } 19 | 20 | pub fn load_uri(&self, uri: &'static str) { 21 | let raw_uri = CString::new(uri).unwrap().as_ptr(); 22 | unsafe { webkit_web_view_load_uri(self.webkit_webview(), raw_uri); } 23 | } 24 | 25 | pub fn go_back(&self) { 26 | unsafe { webkit_web_view_go_back(self.webkit_webview()) } 27 | } 28 | 29 | pub fn go_forward(&self) { 30 | unsafe { webkit_web_view_go_forward(self.webkit_webview()) } 31 | } 32 | 33 | pub fn focus(&self) { 34 | unsafe { gtk_sys::gtk_widget_show_all(self.gtk_widget) } 35 | } 36 | 37 | fn webkit_webview(&self) -> *mut WebKitWebView { 38 | unsafe { transmute(self.gtk_widget) } 39 | } 40 | } 41 | 42 | impl GObjectConvertible for WebView { 43 | 44 | fn gobject(&self) -> *mut GObject { 45 | unsafe { transmute(self.gtk_widget) } 46 | } 47 | } 48 | 49 | impl GtkWidgetConvertible for WebView { 50 | 51 | fn gtk_widget(&self) -> *mut GtkWidget { 52 | self.gtk_widget 53 | } 54 | } 55 | 56 | impl Drop for WebView { 57 | 58 | fn drop(&mut self) { 59 | unsafe { g_object_unref(self.gobject()); } 60 | } 61 | } 62 | 63 | fn create_gtk_widget() -> *mut GtkWidget { 64 | return unsafe { 65 | let widget = webkit_web_view_new(); 66 | let gobject: *mut GObject = transmute(widget); 67 | g_object_ref(gobject); 68 | widget 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /webkitten-gtk/src/ui/window.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | use std::ffi::CString; 3 | 4 | use super::gtk_sys; 5 | use super::gtk_sys::{GtkWindow,GtkContainer,GtkWidget}; 6 | use super::{WebView, 7 | BoxContainer, 8 | TextField, 9 | FlowDirection, 10 | GtkContainerConvertible, 11 | GtkWidgetConvertible}; 12 | 13 | pub struct Window { 14 | address_field: TextField, 15 | command_field: TextField, 16 | gtk_widget: *mut GtkWidget, 17 | webview_container: BoxContainer, 18 | webviews: Vec, 19 | widget_container: BoxContainer, 20 | did_layout: bool, 21 | } 22 | 23 | impl Window { 24 | 25 | pub fn new(title: &'static str) -> Self { 26 | Window { 27 | address_field: TextField::new(), 28 | command_field: TextField::new(), 29 | gtk_widget: create_gtk_widget(title), 30 | webview_container: BoxContainer::new(FlowDirection::Vertical), 31 | widget_container: BoxContainer::new(FlowDirection::Vertical), 32 | webviews: vec![], 33 | did_layout: false 34 | } 35 | } 36 | 37 | pub fn add_webview(&mut self, uri: &'static str) { 38 | if !self.did_layout { 39 | self.layout(); 40 | } 41 | let webview = WebView::new(); 42 | self.webview_container.add_child(webview.gtk_widget(), true); 43 | webview.load_uri(uri); 44 | webview.focus(); 45 | 46 | self.webviews.push(webview); 47 | } 48 | 49 | pub fn add_child(&mut self, child: *mut GtkWidget) { 50 | unsafe { gtk_sys::gtk_container_add(self.gtk_container(), child) } 51 | } 52 | 53 | pub fn show(&mut self) { 54 | unsafe { gtk_sys::gtk_widget_show_all(self.gtk_widget); } 55 | } 56 | 57 | pub fn set_size(&mut self, width: i32, height: i32) { 58 | unsafe { gtk_sys::gtk_window_set_default_size(self.gtk_window(), 59 | width, height) } 60 | } 61 | 62 | fn gtk_window(&self) -> *mut GtkWindow { 63 | unsafe { transmute(self.gtk_widget) } 64 | } 65 | 66 | fn layout(&mut self) { 67 | self.widget_container.add_child(self.command_field.gtk_widget(), false); 68 | self.widget_container.add_child(self.webview_container.gtk_widget(), true); 69 | self.widget_container.add_child(self.address_field.gtk_widget(), false); 70 | let widget_container = self.widget_container.gtk_widget(); 71 | self.add_child(widget_container); 72 | self.did_layout = true; 73 | } 74 | } 75 | 76 | impl GtkContainerConvertible for Window { 77 | 78 | fn gtk_container(&self) -> *mut GtkContainer { 79 | unsafe { transmute(self.gtk_widget) } 80 | } 81 | } 82 | 83 | // Destroy the application when a window is closed - temporary behavior while 84 | // there is only one window per application. 85 | unsafe extern "C" fn destroy_window_callback(_: *mut GtkWidget, _: *mut GtkWidget) { 86 | gtk_sys::gtk_main_quit(); 87 | } 88 | 89 | fn create_gtk_widget(title: &'static str) -> *mut GtkWidget { 90 | return unsafe { 91 | let widget = gtk_sys::gtk_window_new(gtk_sys::GTK_WINDOW_TOPLEVEL); 92 | super::add_widget_callback(widget, "destroy", destroy_window_callback); 93 | let window: *mut GtkWindow = transmute(widget); 94 | gtk_sys::gtk_window_set_title(window, CString::new(title).unwrap().as_ptr()); 95 | gtk_sys::gtk_window_set_default_size(window, 800, 600); 96 | widget 97 | }; 98 | } 99 | -------------------------------------------------------------------------------- /webkitten-gtk/src/webkitgtk_sys/mod.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | extern crate gtk_sys; 3 | 4 | use libc::c_char; 5 | use gtk_sys::GtkWidget; 6 | 7 | pub enum WebKitWebView {} 8 | 9 | #[link(name="webkit2gtk-4.0")] 10 | extern "C" { 11 | pub fn webkit_web_view_new() -> *mut GtkWidget; 12 | pub fn webkit_web_view_load_uri(webview: *mut WebKitWebView, uri: *const c_char); 13 | pub fn webkit_web_view_go_back(webview: *mut WebKitWebView); 14 | pub fn webkit_web_view_go_forward(webview: *mut WebKitWebView); 15 | } 16 | 17 | --------------------------------------------------------------------------------