Compare commits

..

No commits in common. 'master' and 'v1.0.0' have entirely different histories.

@ -13,17 +13,17 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -32,7 +32,6 @@ jobs:
with:
profile: minimal
toolchain: stable
components: clippy
override: true
- name: check
run: cargo check
@ -49,17 +48,17 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

@ -3,7 +3,7 @@ name: Create Release
on:
push:
tags:
- "v*.*.*"
- 'v*.*.*'
jobs:
build_armv7:
@ -13,17 +13,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -50,50 +50,6 @@ jobs:
name: phetch-linux-armv7
path: target/armv7-unknown-linux-gnueabihf/release/phetch-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
build_aarch64:
name: Build for android aarch64
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Setup Toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: aarch64-linux-android
override: true
- name: Build release
uses: actions-rs/cargo@v1
with:
use-cross: true
command: build
args: --release --locked --target aarch64-linux-android
- name: Get current version
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}
- name: Package Binary
run: cp doc/phetch.1 target/aarch64-linux-android/release && cd target/aarch64-linux-android/release && tar zcvf phetch-${{ steps.get_version.outputs.VERSION }}-android-aarch64.tgz phetch phetch.1
- name: Upload Artifact
uses: actions/upload-artifact@v1
with:
name: phetch-android-aarch64
path: target/aarch64-linux-android/release/phetch-${{ steps.get_version.outputs.VERSION }}-android-aarch64.tgz
build_linux:
name: Build for Linux x86_64
runs-on: ubuntu-latest
@ -101,17 +57,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v2
- name: Cache cargo registry
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -134,17 +90,17 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: Cache cargo registry
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v3
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@ -172,7 +128,7 @@ jobs:
create:
name: Create Release
needs: [build_armv7, build_aarch64, build_linux, build_macos]
needs: [build_armv7, build_linux, build_macos]
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -192,20 +148,15 @@ jobs:
uses: actions/download-artifact@v1
with:
name: phetch-linux-armv7
- name: Download Android (aarch64) artifact
uses: actions/download-artifact@v1
with:
name: phetch-android-aarch64
- name: Create Release
uses: softprops/action-gh-release@v1
with:
draft: true
prerelease: false
prerelease: true
files: |
phetch-macos/phetch-${{ steps.get_version.outputs.VERSION }}-macos.zip
phetch-linux-x86_64/phetch-${{ steps.get_version.outputs.VERSION }}-linux-x86_64.tgz
phetch-linux-armv7/phetch-${{ steps.get_version.outputs.VERSION }}-linux-armv7.tgz
phetch-android-aarch64/phetch-${{ steps.get_version.outputs.VERSION }}-android-aarch64.tgz
body_path: CHANGELOG.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

3
.gitignore vendored

@ -2,5 +2,4 @@
**/*.rs.bk
phetch
phetch.log
.criterion/
conf
.criterion/

@ -1,190 +1,3 @@
## v1.2.1-dev
- Fix `NO_COLOR` support.
- The `d` keyboard shortcut will now download the current page to disk.
## v1.2.0
phetch is all about fun colors, but your options have always been
limited. You could turn off colors via `NO_COLOR` env variable or
you could leave them on. That's it.
Well, not anymore. As of `v1.2`, phetch now supports themes, in
addition to a few new config options.
### Config Options
The new config options in this release, for your convenience, are
as follows:
- `scroll` controls how many lines to jump by when paging up/down.
If set to 0 (the new default), you'll jump by an entire screen.
- `autoplay` controls whether you'll be prompted to play media files
or not. By default it's false, but one might find it handy to set
to `true` if hosting, say, a Gopher-powered music server.
### Keyboard Shortcuts
Just one, but it's a doozy - you can now reload the current URL by
pressing `R` (`shift+r`). Super handy when developing your own Gopherhole!
### Themes
As mentioned, themes are simple files with roughly the same format as
`phetch.conf`:
$ cat ~/.config/phetch/default.theme
# Color Scheme
## UI
ui.cursor white bold
ui.number magenta
ui.menu yellow
ui.text white
## Items
item.text cyan
item.menu blue
item.error red
item.search white
item.telnet grey
item.external green
item.download white underline
item.media green underline
item.unsupported whitebg red
Create your theme file and launch phetch with `-t FILE`, or set
the `theme FILE` option in your `~/.config/phetch/phetch.conf`
You can also set theme colors directly in your `phetch.conf`.
Learn more about themes, including which colors are available,
by opening phetch's on-line help: press `h` then `7` to get
there quickly.
For reference, we've included a few fun themes in the repo itself
that you can download and play with:
<https://github.com/xvxx/phetch/tree/master/themes>
## v1.1.0
Three new features in this release, plus an unknown number of new
bugs:
1. When the `NO_COLOR` env variable is set, phetch won't use colors
when rendering menus. See https://no-color.org/ for more information.
2. CP437 encoding support! You can toggle it on or off using `ctrl-e`
(for encoding) when viewing a Gopher text document, or using the
`--encoding` command line flag. See
https://en.wikipedia.org/wiki/Code_page_437.
Huge thanks to Kjell for suggesting this feature and providing some
great test data!
_NOTE: This only works for text documents since there's no `TAB`
character in CP437._
3. phetch now supports a primitive form of wrapping long lines when
rendering Gopher text documents. It won't reflow the text, but it
will make some phlogs and other documents slightly more readable.
Enable it with `--wrap NUM` or by adding `wrap NUM` to your
`phetch.conf`. You can disable it with `wrap 0`.
---
You may have run into long lines that don't break at the margins,
making the page hard to scroll and read:
![not wrapped](https://user-images.githubusercontent.com/41523880/97058194-f73d9d80-1541-11eb-8fc8-910489fafcc3.png)
Now, by either passing `--wrap NUM` or adding `wrap NUM` to your
`phetch.conf` file, phetch will attempt to wrap long lines at the
nearest punctation or space:
![wrapped](https://user-images.githubusercontent.com/41523880/97058201-fa388e00-1541-11eb-84ef-c539304870a6.png)
This is really useful if you want to browse, say, a directory of
Markdown files over Gopher. Modern Markdown is often written with the
assumption that the client will do the wrapping, so it can end up
looking pretty messy in an ananchronistic client like phetch. Reading
those files is now a bit easier:
| not wrapped | wrapped |
| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| ![not wrapped](https://user-images.githubusercontent.com/41523880/97057857-1556ce00-1541-11eb-9cc1-6c6d438529ea.png) | ![wrapped](https://user-images.githubusercontent.com/41523880/97057869-1ee03600-1541-11eb-8e7b-ae47ff9ec871.png) |
This also works nicely on native Gopher content: phlog entries
sometimes have long URLs in their footnotes, and that could screw up
phetch's margin calculations.
Note that this doesn't do any _reflow_ of text, so documents with long
lines will still look a bit wonky, as you can see above. Some lines
will be too short. But it's a lot more usable, so we'll take it!
PS: You can use smaller values to get weird with it:
![weird](https://user-images.githubusercontent.com/41523880/97057878-269fda80-1541-11eb-9435-89f97cce8825.png)
Enjoy!
## v1.0.7
This release fixes https://github.com/xvxx/phetch/issues/19
phetch was aborting whenever it encountered a connection error
instead of trying the alternate socket addrs it was given.
Special thanks to @Ramiferous and @voidpin and **rvp**!
## v1.0.6
- More "reload" bugfixes.
## v1.0.5
Fix a crash introduced in 1.0.4.
## v1.0.4
- The `ctrl-u` and `ctrl-g` keyboard shortcuts can now be used
to reload the current page.
## v1.0.3
This release adds support for the `;` and `s` Gopher item types,
as well as the ability to play them in a media player - meaning
you can now run Gopher-powered media servers! As seen here:
https://twitter.com/grufwub/status/1264296292764856320
`mpv` is used by default, but you can specify a custom player
or disable the feature using the `-m` and `-M` flags. Info has
been added to `--help` and the phetch manual.
Special thanks to @grufwub for the feature request and getting
the code rolling!
Enjoy!
## v1.0.2
This release fixes a few small but irritating bugs:
- ANSI color codes now render properly. Full technicolor support.
Try it out: `phetch gopher://tilde.black/1/users/genin/`
- Resizing your terminal now resizes phetch automatically.
- Downloads can now be cancelled while in-progress with no funny
business.
- Debug information is now properly displayed when phetch crashes.
## v1.0.1
This is a small bugfix release. Thanks to @TheEnbyperor and @grufwub!
- phetch no longer panics on multibyte characters when trying to
truncate Gopher content.
## v1.0.0
`phetch` is now **v1.0.0**! Major thanks to @kseistrup for design,
@ -193,11 +6,11 @@ testing, and documentation, @iglosiggio for supporting [GILD][gild],
one year ago with his blog post, [Gopher: a present for
Redis](http://antirez.com/news/127).
---
-----
![phetch screen][phetch screen]
![phetch](https://raw.githubusercontent.com/xvxx/phetch/f1fe58d2483af1c64fa61aa46e5858b599f8e67b/img/start.png)
---
-----
`phetch` is a terminal Gopher client designed to help you quickly
navigate the gophersphere. With a snappy, text-based UI, Gopher types
@ -211,7 +24,7 @@ on how to install for Arch Linux with AUR (`yay phetch`), macOS with
homebrew (`brew install xvxx/code/phetch`), or how to build from
source.
---
-----
I have fond memories of using telnet to connect to the local library
when I was a kid, browsing their selection of books in an
@ -220,7 +33,7 @@ using some version of Windows, literally dialing into the library with
Hyperterminal.
<p align="center">
<img src="https://git.io/JvusG" alt="library tui">
<img src="https://user-images.githubusercontent.com/41523880/75273938-b0cb4f80-57b6-11ea-90e8-f4d759bdbbc0.png" alt="library tui">
</p>
It was futuristic. And, I thought, lost in the past. But Gopher, a
@ -231,11 +44,11 @@ The protocol is simple, constrained, and bursting with opportunity.
And while [MTV may not have an active Gopher server anymore][mtv], you
can easily run your own, or find a generous host like SDF or a tilde.
---
-----
![gopher menu in phetch][phetch menu]
![gopher menu in phetch](https://raw.githubusercontent.com/xvxx/phetch/3ec5e3f4335a5fdf709b5643da8aa4d5abe70815/img/dos.png)
---
-----
`phetch` is my attempt to bring a little bit of that retro-nostalgia
back into my terminal. Sure, I can acccess Gopher just fine using
@ -246,8 +59,6 @@ To get started just install and run `phetch`.
It's not perfect, but I've had fun using it, and I hope you do too!
[phetch screen]: https://raw.githubusercontent.com/xvxx/phetch/f1fe58d2483af1c64fa61aa46e5858b599f8e67b/img/start.png
[phetch menu]: https://raw.githubusercontent.com/xvxx/phetch/3ec5e3f4335a5fdf709b5643da8aa4d5abe70815/img/dos.png
[install]: README.md#installation
[gild]: https://github.com/xvxx/gild
[floodgap]: https://gopher.floodgap.com/gopher/

923
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
[package]
name = "phetch"
version = "1.2.0"
version = "1.0.0"
authors = ["chris west <c@xvxx.io>"]
license = "MIT"
edition = "2018"
@ -29,15 +29,28 @@ opt-level = 'z' # Optimize for size.
pre-release-replacements = [
{file="README.md", search="phetch-v\\d+\\.\\d+\\.\\d+-", replace="{{crate_name}}-v{{version}}-"},
{file="README.md", search="/v\\d+\\.\\d+\\.\\d+/", replace="/v{{version}}/"},
{file="CHANGELOG.md", search="\\d+\\.\\d+\\.\\d+-dev", replace="{{version}}"},
]
dev-version-ext = "dev"
[dev-dependencies]
criterion = "0.3.1"
[[bench]]
name = "parse_gopher"
harness = false
[[bench]]
name = "render_menu"
harness = false
[[bench]]
name = "render_text"
harness = false
[dependencies]
termion = "1.5.5"
libc = "0.2.66"
atty = "0.2.14"
lazy_static = "1.4"
cp437 = "0.1.1"
tor-stream = { version = "0.2.0", optional = true }
native-tls = { version = "0.2", optional = true }

@ -1,4 +1,2 @@
[target.armv7-unknown-linux-gnueabihf]
image = "rustembedded/cross:armv7-unknown-linux-gnueabihf-0.1.16"
[target.aarch64-linux-android]
image = "rustembedded/cross:aarch64-linux-android-0.1.16"

@ -20,24 +20,18 @@ release: $(PHETCH_RELEASE)
# Binary with debugging info
debug: $(PHETCH_DEBUG)
./target/debug/phetch
# Remove the release directory and its contents
clean:
@rm -rf target
# Run tests
test:
cargo clippy --all-features
cargo test --all-features
# Build the release version
$(PHETCH_RELEASE): $(RSFILES)
cargo build --release
# Build the debug version
$(PHETCH_DEBUG): $(RSFILES)
cargo build --no-default-features
cargo build
# Install phetch and its manual.
install: all

@ -6,14 +6,11 @@
|
--> <p align="center"> <img src="./img/logo.png"> <br>
<a href="https://git.io/JveQo">
<img src="https://img.shields.io/github/v/release/xvxx/phetch">
<img src="https://img.shields.io/github/v/release/xvxx/phetch?include_prereleases">
</a>
<a href="https://crates.io/crates/phetch">
<img src="https://img.shields.io/crates/v/phetch">
</a>
<a href="https://aur.archlinux.org/packages/phetch/">
<img src="https://img.shields.io/aur/version/phetch">
</a>
<a href="https://git.io/JvR5g">
<img src="https://github.com/xvxx/phetch/workflows/build/badge.svg">
</a>
@ -28,7 +25,7 @@ the gophersphere.
## features
- <1MB executable for Linux, Mac, and NetBSD
- <1MB executable for Linux and Mac
- Technicolor design (based on [GILD](https://github.com/xvxx/gild))
- No-nonsense keyboard navigation
- Supports Gopher searches, text and menu pages, and downloads
@ -39,8 +36,6 @@ the gophersphere.
## usage
Usage:
phetch [options] Launch phetch in interactive mode
phetch [options] url Open Gopher URL in interactive mode
@ -50,21 +45,12 @@ the gophersphere.
-o, --tor Use local Tor proxy to open all pages
-S, -O Disable TLS or Tor
-w, --wrap COLUMN Wrap long lines in "text" views at COLUMN.
-m, --media PROGRAM Use to open media files. Default: mpv
-M, --no-media Just download media files, don't download
-a, --autoplay Autoplay media files without prompting.
-A, --no-autoplay Prompt before playing media files.
-r, --raw Print raw Gopher response only
-p, --print Print rendered Gopher response only
-l, --local Connect to 127.0.0.1:7070
-e, --encoding Render text documents in CP437 or UTF8.
-c, --config FILE Use instead of ~/.config/phetch/phetch.conf
-C, --no-config Don't use any config file
-t, --theme FILE Use FILE for color theme or print current theme.
--print-theme Print current theme.
-h, --help Show this screen
-v, --version Show phetch version
@ -79,33 +65,22 @@ If you already have a Gopher client, download `phetch` here:
gopher://phkt.io/1/phetch/latest
On macOS you can install with [Homebrew](https://brew.sh/)
or [MacPorts](https://macports.org).
If you're using Homebrew, open the Terminal and type:
On macOS you can install with [Homebrew](https://brew.sh/):
brew install xvxx/code/phetch
For MacPorts:
sudo port install phetch
On Arch Linux, install phetch with your favorite [AUR helper][aur]:
yay phetch
On NetBSD, phetch is included in the main pkgsrc repo:
pkgin install phetch
Binaries for Linux, Raspberry Pi, Mac and Android (termux) are available at
Binaries for Linux, Raspberry Pi, and Mac are available at
https://github.com/xvxx/phetch/releases:
- [phetch-v1.2.0-linux-x86_64.tgz][0]
- [phetch-v1.2.0-linux-armv7.tgz (Raspberry Pi)][1]
- [phetch-v1.2.0-macos.zip][2]
- [phetch-v1.0.0-linux-x86_64.tgz][0]
- [phetch-v1.0.0-linux-armv7.tgz (Raspberry Pi)][1]
- [phetch-v1.0.0-macos.zip][2]
Just unzip/untar the `phetch` program into your `$PATH` and get going!
Just unzip/untar the `phetch` program into your $PATH and get going!
You can also build and install from source if you have `cargo`,
`make`, and the other dependencies described in the next section:
@ -114,10 +89,6 @@ You can also build and install from source if you have `cargo`,
cd phetch
env PREFIX=/usr/local make install
For Termux use:
env PREFIX=/data/data/com.termux/files/usr make install
## development
To build with TLS support on **Linux**, you need `openssl` and
@ -129,9 +100,9 @@ Regular development uses `cargo`:
cargo run -- <gopher-url>
_Pro-tip:_ Run a local gopher server (like [phd]) on `0.0.0.0:7070`
and start phetch with `-l` or `--local` to quickly connect to it.
Useful for debugging!
*Pro-tip:* Run a local gopher server (like [phd][phd]) on
`127.0.0.1:7070` and start phetch with `-l` or `--local` to quickly
connect to it.
phetch builds with TLS and Tor support by default. To disable these
features, or to enable only one of them, use the
@ -147,21 +118,18 @@ To enable just TLS support, or just Tor support, use `--features`:
cargo run --no-default-features --features tor -- gopher://phetch/about
## media player support
phetch includes support for opening video files (`;` item type) and
sound files (`s` item type) in [mpv] or an application of your choice
using the `-m` command line flag. To test it out, visit a compatible
Gopher server (maybe one using [Gophor]?). Or check out the "gopher
types" help page by pressing `ctrl-h` then `3` in phetch.
## todo
- [ ] ctrl-c in load() not yet implemented
- [ ] catch SIGWINCH
## bugs
- [ ] telnet IO seems broken after raw_input change (1146f42)
- [ ] ctrl-c while telneting kills phetch
- [ ] ctrl-c in load() not yet implemented
- [ ] ctrl-c in download fails to return to listening state
because of termion bug:
https://gitlab.redox-os.org/redox-os/termion/issues/168
- [ ] gopher://tilde.black/1/users/genin/
## future features
@ -173,10 +141,8 @@ types" help page by pressing `ctrl-h` then `3` in phetch.
- [ ] bookmarks: toggle instead of just prepending to the file
- [ ] bookmarks: save the title of the current page
[0]: https://github.com/xvxx/phetch/releases/download/v1.2.0/phetch-v1.2.0-linux-x86_64.tgz
[1]: https://github.com/xvxx/phetch/releases/download/v1.2.0/phetch-v1.2.0-linux-armv7.tgz
[2]: https://github.com/xvxx/phetch/releases/download/v1.2.0/phetch-v1.2.0-macos.zip
[0]: https://github.com/xvxx/phetch/releases/download/v1.0.0/phetch-v1.0.0-linux-x86_64.tgz
[1]: https://github.com/xvxx/phetch/releases/download/v1.0.0/phetch-v1.0.0-linux-armv7.tgz
[2]: https://github.com/xvxx/phetch/releases/download/v1.0.0/phetch-v1.0.0-macos.zip
[phd]: https://github.com/xvxx/phd
[aur]: https://wiki.archlinux.org/index.php/AUR_helpers
[mpv]: https://github.com/mpv-player/mpv
[gophor]: https://github.com/grufwub/gophor

@ -1,5 +0,0 @@
/target
**/*.rs.bk
phetch
phetch.log
.criterion/

918
benchmarks/Cargo.lock generated

@ -1,918 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi 0.3.9",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bstr"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31accafdb70df7871592c058eca3985b71104e15ac32f64706022c58867da931"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bumpalo"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
[[package]]
name = "byteorder"
version = "1.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
[[package]]
name = "cast"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b9434b9a5aa1450faa3f9cb14ea0e8c53bb5d2b3c1bfd1ab4fc03e9f33fbfb0"
dependencies = [
"rustc_version",
]
[[package]]
name = "cc"
version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "clap"
version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [
"bitflags",
"textwrap",
"unicode-width",
]
[[package]]
name = "core-foundation"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
[[package]]
name = "criterion"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70daa7ceec6cf143990669a04c7df13391d55fb27bd4079d252fca774ba244d8"
dependencies = [
"atty",
"cast",
"clap",
"criterion-plot",
"csv",
"itertools",
"lazy_static",
"num-traits",
"oorandom",
"plotters",
"rayon",
"regex",
"serde",
"serde_cbor",
"serde_derive",
"serde_json",
"tinytemplate",
"walkdir",
]
[[package]]
name = "criterion-plot"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e022feadec601fba1649cfa83586381a4ad31c6bf3a9ab7d408118b05dd9889d"
dependencies = [
"cast",
"itertools",
]
[[package]]
name = "crossbeam-channel"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87"
dependencies = [
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-deque"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02af974daeee82218205558e51ec8768b48cf524bd01d550abe5573a608285"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
"maybe-uninit",
]
[[package]]
name = "crossbeam-epoch"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace"
dependencies = [
"autocfg",
"cfg-if",
"crossbeam-utils",
"lazy_static",
"maybe-uninit",
"memoffset",
"scopeguard",
]
[[package]]
name = "crossbeam-utils"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
dependencies = [
"autocfg",
"cfg-if",
"lazy_static",
]
[[package]]
name = "csv"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00affe7f6ab566df61b4be3ce8cf16bc2576bca0963ceb0955e45d514bf9a279"
dependencies = [
"bstr",
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90"
dependencies = [
"memchr",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "getrandom"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "half"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36fab90f82edc3c747f9d438e06cf0a491055896f2a279638bb5beed6c40177"
[[package]]
name = "hermit-abi"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c30f6d0bc6b00693347368a67d41b58f2fb851215ff1da49e90fe2c5c667151"
dependencies = [
"libc",
]
[[package]]
name = "itertools"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6"
[[package]]
name = "js-sys"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca059e81d9486668f12d455a4ea6daa600bd408134cd17e3d3fb5a32d1f016f8"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235"
[[package]]
name = "log"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b"
dependencies = [
"cfg-if",
]
[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
[[package]]
name = "memchr"
version = "2.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
[[package]]
name = "memoffset"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa"
dependencies = [
"autocfg",
]
[[package]]
name = "native-tls"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b0d88c06fe90d5ee94048ba40409ef1d9315d86f6f38c2efdaad4fb50c58b2d"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "num-traits"
version = "0.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "oorandom"
version = "11.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a170cebd8021a008ea92e4db85a72f80b35df514ec664b296fdcbb654eac0b2c"
[[package]]
name = "openssl"
version = "0.10.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"lazy_static",
"libc",
"openssl-sys",
]
[[package]]
name = "openssl-probe"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
[[package]]
name = "openssl-sys"
version = "0.9.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "phetch"
version = "1.0.4-dev"
dependencies = [
"atty",
"lazy_static",
"libc",
"native-tls",
"termion",
"tor-stream",
]
[[package]]
name = "phetch-benchmarks"
version = "0.1.0"
dependencies = [
"criterion",
"phetch",
]
[[package]]
name = "pkg-config"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
[[package]]
name = "plotters"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d1685fbe7beba33de0330629da9d955ac75bd54f33d7b79f9a895590124f6bb"
dependencies = [
"js-sys",
"num-traits",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "ppv-lite86"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
[[package]]
name = "proc-macro2"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom",
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core",
]
[[package]]
name = "rayon"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfd016f0c045ad38b5251be2c9c0ab806917f82da4d36b2a327e5166adad9270"
dependencies = [
"autocfg",
"crossbeam-deque",
"either",
"rayon-core",
]
[[package]]
name = "rayon-core"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8c4fec834fb6e6d2dd5eece3c7b432a52f0ba887cf40e595190c4107edc08bf"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils",
"lazy_static",
"num_cpus",
]
[[package]]
name = "redox_syscall"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce"
[[package]]
name = "redox_termios"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
dependencies = [
"redox_syscall",
]
[[package]]
name = "regex"
version = "1.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4"
dependencies = [
"byteorder",
]
[[package]]
name = "regex-syntax"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "rustc_version"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
dependencies = [
"lazy_static",
"winapi 0.3.9",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64808902d7d99f78eaddd2b4e2509713babc3dc3c85ad6f4c447680f3c01e535"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bf11d99252f512695eb468de5516e5cf75455521e69dfe343f3b74e4748405"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser",
]
[[package]]
name = "semver-parser"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96fe57af81d28386a513cbc6858332abc6117cfdb5999647c6444b8f43a370a5"
[[package]]
name = "serde_cbor"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.116"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f630a6370fd8e457873b4bd2ffdae75408bc291ba72be773772a4c2a065d9ae8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "socks"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a64cfa9346d26e836a49fcc1ddfcb4d3df666b6787b6864db61d4918e1cbc2"
dependencies = [
"byteorder",
"libc",
"winapi 0.2.8",
"ws2_32-sys",
]
[[package]]
name = "syn"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c51d92969d209b54a98397e1b91c8ae82d8c87a7bb87df0b29aa2ad81454228"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "tempfile"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
dependencies = [
"cfg-if",
"libc",
"rand",
"redox_syscall",
"remove_dir_all",
"winapi 0.3.9",
]
[[package]]
name = "termion"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
dependencies = [
"libc",
"numtoa",
"redox_syscall",
"redox_termios",
]
[[package]]
name = "textwrap"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
]
[[package]]
name = "tinytemplate"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d3dc76004a03cec1c5932bca4cdc2e39aaa798e3f82363dd94f9adf6098c12f"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "tor-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5865109fc90e0bc0f8c299f3794ca0fd5771df988aa6b962d4c9129c39674746"
dependencies = [
"lazy_static",
"socks",
]
[[package]]
name = "unicode-width"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "vcpkg"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
[[package]]
name = "walkdir"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
dependencies = [
"same-file",
"winapi 0.3.9",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasm-bindgen"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ac64ead5ea5f05873d7c12b545865ca2b8d28adfc50a49b84770a3a97265d42"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f22b422e2a757c35a73774860af8e112bff612ce6cb604224e8e47641a9e4f68"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b13312a745c08c469f0b292dd2fcd6411dba5f7160f593da6ef69b64e407038"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f249f06ef7ee334cc3b8ff031bfc11ec99d00f34d86da7498396dc1e3b1498fe"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d649a3145108d7d3fbcde896a468d1bd636791823c9921135218ad89be08307"
[[package]]
name = "web-sys"
version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bf6ef87ad7ae8008e15a355ce696bed26012b7caa21605188cfd8214ab51e2d"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi 0.3.9",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]

@ -1,23 +0,0 @@
[package]
name = "phetch-benchmarks"
version = "0.1.0"
authors = ["chris west <c@xvxx.io>"]
edition = "2018"
[dev-dependencies]
criterion = "0.3.1"
[[bench]]
name = "parse_gopher"
harness = false
[[bench]]
name = "render_menu"
harness = false
[[bench]]
name = "render_text"
harness = false
[dependencies]
phetch = { path = ".." }

@ -1,3 +0,0 @@
fn main() {
println!("Run `cargo bench`");
}

@ -1,11 +1,10 @@
.\" Generated by scdoc 1.11.2
.\" Complete documentation for this program is not available as a GNU info page
.\" Generated by scdoc 1.10.0
.ie \n(.g .ds Aq \(aq
.el .ds Aq '
.nh
.ad l
.\" Begin generated content:
.TH "PHETCH" "1" "2022-11-14"
.TH "PHETCH" "1" "2020-01-12"
.P
.SH NAME
.P
@ -18,182 +17,139 @@ phetch - quick lil gopher client
.SH DESCRIPTION
.P
\fBphetch\fR is a terminal client designed to help you quickly navigate
the gophersphere.\& It features non-nonsense keyboard navigation,
the gophersphere. It features non-nonsense keyboard navigation,
support for most Gopher features, easy-to-use TLS and Tor support, as
well as bookmarking and history features.\&
well as bookmarking and history features.
.P
Usually \fBphetch\fR is started with a Gopher URL:
.P
.RS 4
phetch gopher://some-gopher-url.\&com
phetch gopher://some-gopher-url.com
.P
.RE
If no URL is given, however, \fBphetch\fR will launch and open its default
"start page".\& This can be configured to be any URL.\& (See \fBCONFIG\fR.\&)
"start page". This can be configured to be any URL. (See \fBCONFIG\fR.)
.P
.SH OPTIONS
.P
\fB-l\fR, \fB--local\fR
.RS 4
Connect to the local Gopher server at URL \fI127.\&0.\&0.\&1:7070\fR.\&
Connect to the local Gopher server at URL \fI127.0.0.1:7070\fR.
.P
.RE
\fB-p\fR \fIURL\fR, \fB--print\fR \fIURL\fR
.RS 4
Print a rendered Gopher server response of \fIURL\fR and exit.\&
Print a rendered Gopher server response of \fIURL\fR and exit.
.P
.RE
\fB-r\fR \fIURL\fR, \fB--raw\fR \fIURL\fR
.RS 4
Print the raw Gopher server response of \fIURL\fR and exit.\&
Print the raw Gopher server response of \fIURL\fR and exit.
.P
.RE
\fB-s\fR, \fB--tls\fR
.RS 4
Attempt to fetch all pages securely over TLS.\&
Attempt to fetch all pages securely over TLS.
.P
.RE
\fB-S\fR, \fB--no-tls\fR
.RS 4
Do not use TLS for connections.\& This can be used to cancel out an
option set in the config file, for instance.\&
Do not use TLS for connections. This can be used to cancel out an
option set in the config file, for instance.
.P
.RE
\fB-o\fR, \fB--tor\fR
.RS 4
Make all connections using a local Tor proxy.\&
Tor is The Onion Router.\&
Make all connections using a local Tor proxy.
Tor is The Onion Router.
Set the TOR_PROXY env variable to use an address other than the
Tor default of 127.\&0.\&0.\&1:9050.\&
Tor default of 127.0.0.1:9050.
.P
.RE
\fB-O\fR, \fB--no-tor\fR
.RS 4
Disable Tor.\&
.P
.RE
\fB-w\fR, \fB--wrap\fR \fICOLUMN\fR
.RS 4
Wrap long lines in Gopher "text" views at \fICOLUMN\fR.\&
Default: 0 (off)
.P
.RE
\fB-m\fR, \fB--media\fR \fIPATH\fR
.RS 4
Use program at \fIPATH\fR to open media files (movies and sounds).\&
Default: mpv
.P
.RE
\fB-M\fR, \fB--no-media\fR
.RS 4
Don'\&t try to open media files.\& Download them like regular binary
Gopher items.\&
.P
.RE
\fB-a\fR, \fB--autoplay\fR
.RS 4
Autoplay media files instead of prompting.\&
.P
.RE
\fB-A\fR, \fB--no-autoplay\fR
.RS 4
Don'\&t autoplay media files.\& Prompt instead.\&
Disable Tor.
.P
.RE
\fB-c\fR, \fB--config\fR \fIFILE\fR
.RS 4
Use \fIFILE\fR instead of \fI~/.\&config/phetch/phetch.\&conf\fR
Use \fIFILE\fR instead of \fI~/.config/phetch/phetch.conf\fR
.P
.RE
\fB-C\fR, \fB--no-config\fR
.RS 4
Do not use any config file.\&
.P
.RE
\fB-t\fR, \fB--theme\fR \fIFILE\fR
.RS 4
Use \fIFILE\fR for color theme.\&
.P
.RE
\fB--print-theme\fR
.RS 4
Print current theme.\&
.P
.RE
\fB-e\fR, \fB--encoding\fR \fIENCODING\fR
.RS 4
Render text views in CP437 or UTF8 (default) encoding.\&
Do not use any config file.
.P
.RE
\fB-h\fR, \fB--help\fR
.RS 4
Print a help summary and exit.\&
Print a help summary and exit.
.P
.RE
\fB-v\fR, \fB--version\fR
.RS 4
Print version information and exit.\&
Print version information and exit.
.P
.RE
.SH NOTES
.P
When given a \fIURL\fR, \fBphetch\fR will show the requested Gopher page and
enter interactive mode.\&
enter interactive mode.
.P
Without a \fIURL\fR, \fBphetch\fR will show a builtin dashboard with easy
access to online help, bookmarks and history, and enter interactive
mode.\&
mode.
.P
Command line options always override options set in phetch.\&conf.\&
Command line options always override options set in phetch.conf.
.P
.SH NAVIGATION
.P
.SS KEYBOARD SHORTCUTS
.P
All single letter commands also work with the \fBCtrl\fR key: e.\&g.\&, \fBh\fR
and \fBCtrl-h\fR are synonyms.\&
All single letter commands also work with the \fBCtrl\fR key: e.g., \fBh\fR
and \fBCtrl-h\fR are synonyms.
.P
\fBh\fR
.RS 4
Go to builtin help page.\&
Go to builtin help page.
.RE
\fBq\fR
.RS 4
Quit \fBphetch\fR.\&
Quit \fBphetch\fR.
.P
.RE
\fBleft arrow\fR
.RS 4
Go back in history.\&
Go back in history.
.RE
\fBright arrow\fR
.RS 4
Go forward in history.\&
Go forward in history.
.RE
\fBup arrow\fR, \fBp\fR, \fBk\fR
.RS 4
Select previous link.\&
Select previous link.
.RE
\fBdown arrow\fR, \fBn\fR, \fBj\fR
.RS 4
Select next link.\&
Select next link.
.RE
\fBPgUp\fR, \fB-\fR
.RS 4
Scroll up by many lines.\&
Scroll up by many lines.
.RE
\fBPgDn\fR, \fBSPACE\fR
.RS 4
Scroll down by many lines.\&
Scroll down by many lines.
.P
.RE
\fBNumber key\fR
.RS 4
Open/select link.\&
Open/select link.
.RE
\fBEnter\fR
.RS 4
Open current link.\&
Open current link.
.RE
\fBEsc\fR, \fBCtrl-c\fR
.RS 4
@ -202,50 +158,42 @@ Cancel
.RE
\fBf\fR, \fB/\fR
.RS 4
Find link in page.\&
Find link in page.
.P
.RE
\fBg\fR
.RS 4
Go to Gopher URL.\&
.RE
\fBR\fR
.RS 4
Reload current URL.\&
Go to Gopher URL.
.RE
\fBu\fR
.RS 4
Edit URL.\&
Edit URL.
.RE
\fBy\fR
.RS 4
Copy URL.\&
Copy URL.
.P
.RE
\fBb\fR
.RS 4
Show bookmarks.\&
Show bookmarks.
.RE
\fBs\fR
.RS 4
Save bookmark.\&
Save bookmark.
.RE
\fBa\fR
.RS 4
Show history.\& (Mnemonic: \fBAll\fR pages/history)
Show history. (Mnemonic: \fBAll\fR pages/history)
.P
.RE
\fBr\fR
.RS 4
View raw source.\&
View raw source.
.RE
\fBw\fR
.RS 4
Toggle wide mode.\&
.RE
\fBe\fR
.RS 4
Toggle encoding between UTF8 and CP437.\&
Toggle wide mode.
.P
.RE
.SS MENU NAVIGATION
@ -253,24 +201,24 @@ Toggle encoding between UTF8 and CP437.\&
Up and down arrows
.RS 4
Use the up and down arrows, \fBj\fR and \fBk\fR keys, or \fBn\fR and \fBp\fR
keys to select links.\& \fBphetch\fR will scroll for you, or you can
keys to select links. \fBphetch\fR will scroll for you, or you can
use page up and page down (or \fB-\fR and spacebar) to scroll by
many lines at once.\&
many lines at once.
.P
.RE
Number keys
.RS 4
If there are few enough menu items, pressing a number key will
open a link.\& Otherwise, the first matching number will be
selected.\& Use \fBEnter\fR to open the selected link.\&
open a link. Otherwise, the first matching number will be
selected. Use \fBEnter\fR to open the selected link.
.P
.RE
Incremental search
.RS 4
Press \fBf\fR or \fB/\fR to activate search mode, then just start
typing.\& \fBphetch\fR will look for the first case-insensitive match
and try to select it.\& Use arrow keys or \fBCtrl-p\fR/\fBCtrl-n\fR to cycle
through matches.\&
typing. \fBphetch\fR will look for the first case-insensitive match
and try to select it. Use arrow keys or \fBCtrl-p\fR/\fBCtrl-n\fR to cycle
through matches.
.P
.RE
.SH BOOKMARKS
@ -279,44 +227,44 @@ There are two ways to save the URL of the current page:
.P
\fBy\fR
.RS 4
Copy URL.\&
Copy URL.
.RE
\fBs\fR
.RS 4
Save bookmark.\&
Save bookmark.
.P
.RE
Bookmarks will be saved to the file \fI~/.\&config/phetch/bookmarks.\&gph\fR if
the directory \fI~/.\&config/phetch/\fR exists.\&
Bookmarks will be saved to the file \fI~/.config/phetch/bookmarks.gph\fR if
the directory \fI~/.config/phetch/\fR exists.
.P
\fBb\fR
.RS 4
View saved bookmarks.\&
View saved bookmarks.
.P
.RE
The clipboard function uses \fBpbcopy\fR on MacOS, and \fBxsel\fR \fI-sel clip\fR
on Linux.\&
on Linux.
.P
.SH HISTORY
.P
If you create a \fIhistory.\&gph\fR file in \fI~/.\&config/phetch/\fR, each Gopher
URL you open will be stored there.\&
If you create a \fIhistory.gph\fR file in \fI~/.config/phetch/\fR, each Gopher
URL you open will be stored there.
.P
New URLs are appended to the bottom, but loaded in reverse order, so
you'\&ll see all the most recently visited pages first when you press
the \fBa\fR key.\&
you'll see all the most recently visited pages first when you press
the \fBa\fR key.
.P
Feel free to edit your history file directly, or share it with your
friends!\&
friends!
.P
.SH CONFIG
.P
If you create a \fIphetch.\&conf\fR file in \fI~/.\&config/phetch/\fR, it will be
automatically loaded when \fBphetch\fR starts.\& The config file supports
If you create a \fIphetch.conf\fR file in \fI~/.config/phetch/\fR, it will be
automatically loaded when \fBphetch\fR starts. The config file supports
most command line options, for your convenience, as well as a few ways
to customize your browsing experience.\& For example, \fBphetch\fR will
to customize your browsing experience. For example, \fBphetch\fR will
always launch in TLS mode if `tls yes` appears in the config file --
no need to pass `--tls` or `-t` on startup.\&
no need to pass `--tls` or `-t` on startup.
.P
Here is an example config with all options:
.P
@ -334,114 +282,19 @@ tor no
# Always start in wide mode\&.
wide no
# Program to use to open media files\&.
media mpv
# Use emoji indicators for TLS & Tor\&.
emoji no
# Encoding\&. Only CP437 and UTF8 are supported\&.
encoding utf8
# Wrap text at N columns\&. 0 = off (--wrap)
wrap 0
# How many lines to page up/down by? 0 = full screen
scroll 0
# Path to theme file, if you want to use one
theme ~/\&.config/phetch/dark\&.theme
.fi
.RE
.P
.SH THEMES
.P
You can change phetch'\&s color scheme by supplying your own theme
file with `--theme`/`-t` or by setting `theme FILE` in your
phetch.\&conf.\&
.P
You can also view the current theme with:
.P
.RS 4
$ phetch --print-theme
.P
.RE
Theme files look like this:
.P
.nf
.RS 4
ui\&.cursor white bold
ui\&.number magenta
ui\&.menu yellow
ui\&.text white
item\&.text cyan
item\&.menu blue
item\&.error red
item\&.search white
item\&.telnet grey
item\&.external green
item\&.download white underline
item\&.media green underline
item\&.unsupported whitebg red
.fi
.RE
.P
Valid colors for use in phetch themes:
.P
.nf
.RS 4
bold
underline
grey
red
green
yellow
blue
magenta
cyan
white
black
darkred
darkgreen
darkyellow
darkblue
darkmagenta
darkcyan
darkwhite
blackbg
redbg
greenbg
yellowbg
bluebg
magentabg
cyanbg
whitebg
.fi
.RE
.P
.SH MEDIA PLAYER SUPPORT
.P
\fBphetch\fR includes support for opening video files (`;` item type) and
sound files (`s` item type) in `mpv` or an application of your choice
using the `-m` command line flag.\& To test it out, visit a compatible
Gopher server or check out the "gopher types" help page by lauching
\fBphetch\fR and then pressing `ctrl-h` then `3`.\&
.P
By default \fBphetch\fR will prompt you when you try to open a media file,
but you can change this behavior by starting it with `--autoplay`/`-a`
or by setting `autoplayer true` in your config file.\&
.P
.SH ABOUT
.P
\fBphetch\fR is maintained by chris west, and released under the MIT license.\&
\fBphetch\fR is maintained by chris west, and released under the MIT license.
.P
phetch'\&s Gopher hole:
phetch's Gopher hole:
.RS 4
\fIgopher://phkt.\&io/1/phetch\fR
\fIgopher://phkt.io/1/phetch\fR
.RE
phetch'\&s webpage:
phetch's webpage:
.RS 4
\fIhttps://github.\&com/xvxx/phetch\fR
\fIhttps://github.com/xvxx/phetch\fR

@ -49,39 +49,12 @@ If no URL is given, however, *phetch* will launch and open its default
*-O*, *--no-tor*
Disable Tor.
*-w*, *--wrap* _COLUMN_
Wrap long lines in Gopher "text" views at _COLUMN_.
Default: 0 (off)
*-m*, *--media* _PATH_
Use program at _PATH_ to open media files (movies and sounds).
Default: mpv
*-M*, *--no-media*
Don't try to open media files. Download them like regular binary
Gopher items.
*-a*, *--autoplay*
Autoplay media files instead of prompting.
*-A*, *--no-autoplay*
Don't autoplay media files. Prompt instead.
*-c*, *--config* _FILE_
Use _FILE_ instead of _~/.config/phetch/phetch.conf_
*-C*, *--no-config*
Do not use any config file.
*-t*, *--theme* _FILE_
Use _FILE_ for color theme.
*--print-theme*
Print current theme.
*-e*, *--encoding* _ENCODING_
Render text views in CP437 or UTF8 (default) encoding.
*-h*, *--help*
Print a help summary and exit.
@ -136,8 +109,6 @@ and *Ctrl-h* are synonyms.
*g*
Go to Gopher URL.
*R*
Reload current URL.
*u*
Edit URL.
*y*
@ -154,8 +125,6 @@ and *Ctrl-h* are synonyms.
View raw source.
*w*
Toggle wide mode.
*e*
Toggle encoding between UTF8 and CP437.
## MENU NAVIGATION
@ -200,7 +169,7 @@ If you create a _history.gph_ file in _~/.config/phetch/_, each Gopher
URL you open will be stored there.
New URLs are appended to the bottom, but loaded in reverse order, so
you'll see all the most recently visited pages first when you press
you'll see all the most recently visited pages first when you press
the *a* key.
Feel free to edit your history file directly, or share it with your
@ -230,99 +199,10 @@ tor no
# Always start in wide mode.
wide no
# Program to use to open media files.
media mpv
# Use emoji indicators for TLS & Tor.
emoji no
# Encoding. Only CP437 and UTF8 are supported.
encoding utf8
# Wrap text at N columns. 0 = off (--wrap)
wrap 0
# How many lines to page up/down by? 0 = full screen
scroll 0
# Path to theme file, if you want to use one
theme ~/.config/phetch/dark.theme
```
# THEMES
You can change phetch's color scheme by supplying your own theme
file with `--theme`/`-t` or by setting `theme FILE` in your
phetch.conf.
You can also view the current theme with:
$ phetch --print-theme
Theme files look like this:
```
ui.cursor white bold
ui.number magenta
ui.menu yellow
ui.text white
item.text cyan
item.menu blue
item.error red
item.search white
item.telnet grey
item.external green
item.download white underline
item.media green underline
item.unsupported whitebg red
```
Valid colors for use in phetch themes:
```
bold
underline
grey
red
green
yellow
blue
magenta
cyan
white
black
darkred
darkgreen
darkyellow
darkblue
darkmagenta
darkcyan
darkwhite
blackbg
redbg
greenbg
yellowbg
bluebg
magentabg
cyanbg
whitebg
```
# MEDIA PLAYER SUPPORT
*phetch* includes support for opening video files (`;` item type) and
sound files (`s` item type) in `mpv` or an application of your choice
using the `-m` command line flag. To test it out, visit a compatible
Gopher server or check out the "gopher types" help page by lauching
*phetch* and then pressing `ctrl-h` then `3`.
By default *phetch* will prompt you when you try to open a media file,
but you can change this behavior by starting it with `--autoplay`/`-a`
or by setting `autoplayer true` in your config file.
# ABOUT
*phetch* is maintained by chris west, and released under the MIT license.

@ -1,12 +0,0 @@
# phetch release process
- update CHANGELOG.md
- $ make test
- $ cargo release --prev-tag-name=v1.x.x -n
- $ cargo release --prev-tag-name=v1.x.x
- edit https://github.com/xvxx/phetch/releases
- $ cd ../homebrew-code
- $ make VERSION=v1.x.x
[homebrew-code]: https://github.com/xvxx/homebrew-code

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 409 KiB

@ -1,14 +1,14 @@
//! args::parse() is used to parse command line arguments into a
//! Config structure.
use {
crate::{
config::{self, Config},
encoding::Encoding,
ui::Mode,
},
std::{error::Error, fmt, result::Result},
use crate::{
config::{self, Config},
ui::Mode,
};
use std::{error::Error, fmt, result::Result};
#[cfg(not(test))]
use atty;
/// The error returned if something goes awry while parsing the
/// command line arguments.
@ -43,7 +43,7 @@ impl Error for ArgError {
pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
let mut set_nocfg = false;
let mut set_cfg = false;
let mut cfg = Config::default();
let mut cfg = config::default();
// check for config to load / not load first
let mut iter = args.iter();
@ -61,8 +61,12 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
}
set_cfg = true;
if let Some(arg) = iter.next() {
cfg = config::load_file(arg.as_ref())
.map_err(|e| ArgError::new(format!("error loading config: {}", e)))?;
cfg = match config::load_file(arg.as_ref()) {
Ok(c) => c,
Err(e) => {
return Err(ArgError::new(format!("error loading config: {}", e)));
}
};
} else {
return Err(ArgError::new("need a config file"));
}
@ -102,11 +106,6 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
let mut set_notls = false;
let mut set_tor = false;
let mut set_notor = false;
let mut set_media = false;
let mut set_nomedia = false;
let mut set_autoplay = false;
let mut set_noautoplay = false;
while let Some(arg) = iter.next() {
match arg.as_ref() {
"-v" | "--version" | "-version" => {
@ -127,22 +126,8 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
"-p" | "--print" | "-print" => cfg.mode = Mode::Print,
"-l" | "--local" | "-local" => cfg.start = "gopher://127.0.0.1:7070".into(),
"-C" | "--no-config" | "-no-config" => {}
"-c" | "--config" | "-config" => {
iter.next(); // skip arg
}
"-c" | "--config" | "-config" => {}
arg if arg.starts_with("--config=") || arg.starts_with("-config=") => {}
"-t" | "--theme" | "-theme" => {
if let Some(arg) = iter.next() {
cfg.theme = config::load_file(arg.as_ref())
.map_err(|e| ArgError::new(format!("error loading theme: {}", e)))?
.theme;
} else {
return Err(ArgError::new("need a theme file"));
}
}
"--print-theme" => {
cfg.mode = Mode::PrintTheme;
}
"-s" | "--tls" | "-tls" => {
if set_notls {
return Err(ArgError::new("can't set both --tls and --no-tls"));
@ -177,60 +162,6 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
set_notor = true;
cfg.tor = false;
}
"-w" | "--wrap" | "-wrap" => {
if let Some(column) = iter.next() {
if let Ok(col) = column.as_ref().parse() {
cfg.wrap = col;
} else {
return Err(ArgError::new("--wrap expects a COLUMN arg"));
}
} else {
return Err(ArgError::new("--wrap expects a COLUMN arg"));
}
}
"-m" | "--media" | "-media" => {
if set_nomedia {
return Err(ArgError::new("can't set both --media and --no-media"));
}
set_media = true;
if let Some(player) = iter.next() {
cfg.media = Some(player.as_ref().to_string());
} else {
return Err(ArgError::new("--media expects a PROGRAM arg"));
}
}
"-M" | "--no-media" | "-no-media" => {
if set_media {
return Err(ArgError::new("can't set both --media and --no-media"));
}
set_nomedia = true;
cfg.media = None;
}
"-a" | "--autoplay" | "-autoplay" => {
if set_nomedia {
return Err(ArgError::new("can't set both --no-media and --autoplay"));
}
if set_noautoplay {
return Err(ArgError::new("can't set both --autoplay and --no-autoplay"));
}
set_autoplay = true;
cfg.autoplay = true;
}
"-A" | "--no-autoplay" | "-no-autoplay" => {
if set_autoplay {
return Err(ArgError::new("can't set both --autoplay and --no-autoplay"));
}
cfg.autoplay = false;
set_noautoplay = true;
}
"-e" | "--encoding" | "-encoding" => {
if let Some(encoding) = iter.next() {
cfg.encoding = Encoding::from_str(encoding.as_ref())
.map_err(|e| ArgError::new(e.to_string()))?;
} else {
return Err(ArgError::new("--encoding expects an ENCODING arg"));
}
}
arg => {
if arg.starts_with('-') {
return Err(ArgError::new(format!("unknown flag: {}", arg)));
@ -250,9 +181,7 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
#[cfg(not(test))]
{
if !atty::is(atty::Stream::Stdout)
&& !matches!(cfg.mode, Mode::Raw | Mode::Print | Mode::PrintTheme)
{
if !atty::is(atty::Stream::Stdout) && cfg.mode != Mode::Raw {
cfg.mode = Mode::NoTTY;
}
}

@ -0,0 +1,113 @@
//! Terminal colors.
//! Provides a macro to color text as well as sturcts to get their
//! raw ansi codes.
use std::fmt;
/// Shortcut to produce a String colored with one or more colors.
/// Example:
/// ```
/// let s = color_string!("Red string", Red);
/// let x = color_string!("Hyperlink-ish", Blue, Underline);
macro_rules! color_string {
($s:expr, $( $color:ident ),+) => {{
let mut out = String::from("\x1b[");
$( out.push_str(crate::color::$color::code()); out.push_str(";"); )+
out.push('m');
out.push_str(&$s);
out.push_str(crate::color::Reset.as_ref());
out.replace(";m", "m")
}};
}
/// Shortcut to produce a color's ANSI escape code. Don't forget to Reset!
/// ```
/// let mut o = String::new();
/// o.push_str(color!(Blue));
/// o.push_str(color!(Underline));
/// o.push_str("Hyperlinkish.");
/// o.push_str(color!(Reset));
macro_rules! color {
($color:ident) => {
crate::color::$color.as_ref()
};
}
/// Create a color:: struct that can be used with format!.
/// Example:
/// ```
/// define_color(Red, 91);
/// define_color(Reset, 0);
///
/// println!("{}Error: {}{}", color::Red, msg, color::Reset);
macro_rules! define_color {
($color:ident, $code:literal) => {
#[allow(missing_docs)]
pub struct $color;
impl fmt::Display for $color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl $color {
#[allow(missing_docs)]
#[inline]
pub fn code() -> &'static str {
concat!($code)
}
}
impl AsRef<str> for $color {
#[inline]
fn as_ref(&self) -> &'static str {
concat!("\x1b[", $code, "m")
}
}
};
}
define_color!(Reset, 0);
define_color!(Bold, 1);
define_color!(Underline, 4);
define_color!(Grey, 90);
define_color!(Red, 91);
define_color!(Green, 92);
define_color!(Yellow, 93);
define_color!(Blue, 94);
define_color!(Magenta, 95);
define_color!(Cyan, 96);
define_color!(White, 97);
define_color!(Black, 30);
define_color!(DarkRed, 31);
define_color!(DarkGreen, 32);
define_color!(DarkYellow, 33);
define_color!(DarkBlue, 34);
define_color!(DarkMagenta, 35);
define_color!(DarkCyan, 36);
define_color!(DarkWhite, 37);
define_color!(BlackBG, 40);
define_color!(RedBG, 41);
define_color!(GreenBG, 42);
define_color!(YellowBG, 43);
define_color!(BlueBG, 44);
define_color!(MagentaBG, 45);
define_color!(CyanBG, 46);
define_color!(WhiteBG, 47);
#[cfg(test)]
mod tests {
#[test]
fn test_colors() {
assert_eq!(color_string!("Error", Red), "\x1b[91mError\x1b[0m");
assert_eq!(
color_string!("Fancy Pants", Blue, Underline),
"\x1b[94;4mFancy Pants\x1b[0m"
);
assert_eq!(
color_string!("Super-duper-fancy-pants", Magenta, Underline, Bold, BlueBG),
"\x1b[95;4;1;44mSuper-duper-fancy-pants\x1b[0m"
)
}
}

@ -3,33 +3,19 @@
//!
//! An example default config is provided but unused by this module.
use {
crate::{
encoding::Encoding,
phetchdir,
theme::{to_color, Theme},
ui,
},
std::{
collections::HashMap,
fs::OpenOptions,
io::{self, Read, Result},
sync::{Arc, RwLock},
},
use crate::{phetchdir, ui};
use std::{
collections::HashMap,
fs::OpenOptions,
io::{Read, Result},
};
/// Global, shared config.
pub type SharedConfig = Arc<RwLock<Config>>;
/// phetch will look for this file on load.
const CONFIG_FILE: &str = "phetch.conf";
/// Default start page.
const DEFAULT_START: &str = "gopher://phetch/1/home";
/// Default media player.
const DEFAULT_MEDIA_PLAYER: &str = "mpv";
/// Example of what a default phetch.conf would be.
pub const DEFAULT_CONFIG: &str = "## default config file for the phetch gopher client
## gopher://phkt.io/1/phetch
@ -46,41 +32,8 @@ tor no
# Always start in wide mode. (--wide)
wide no
# Program to use to open media files.
media mpv
# Whether to auto play media
autoplay no
# Use emoji indicators for TLS & Tor. (--emoji)
emoji no
# Encoding. Only CP437 and UTF8 are supported.
encoding utf8
# Wrap text at N columns. 0 = off (--wrap)
wrap 0
# How many lines to page up/down by? 0 = full screen
scroll 0
# Path to theme file, if any
# theme ~/.config/phetch/pink.theme
# Inline Theme
ui.cursor white bold
ui.number magenta
ui.menu yellow
ui.text white
item.text cyan
item.menu blue
item.error red
item.search white
item.telnet grey
item.external green
item.download white underline
item.media green underline
item.unsupported whitebg red
";
/// Not all the config options are available in the phetch.conf. We
@ -98,20 +51,8 @@ pub struct Config {
pub wide: bool,
/// Render connection status as emoji
pub emoji: bool,
/// Media player to use.
pub media: Option<String>,
/// Whether to automatically play media
pub autoplay: bool,
/// Default encoding
pub encoding: Encoding,
/// UI mode. Can't be set in conf file.
pub mode: ui::Mode,
/// Column to wrap lines. 0 = off
pub wrap: usize,
/// Scroll by how many lines? 0 = full screen
pub scroll: usize,
/// Color Scheme
pub theme: Theme,
}
impl Default for Config {
@ -122,13 +63,7 @@ impl Default for Config {
tor: false,
wide: false,
emoji: false,
media: Some(DEFAULT_MEDIA_PLAYER.into()),
autoplay: false,
encoding: Encoding::default(),
mode: ui::Mode::default(),
wrap: 0,
scroll: 0,
theme: Theme::default(),
}
}
}
@ -149,7 +84,7 @@ pub fn load() -> Result<Config> {
/// Attempt to load a config from disk.
pub fn load_file(path: &str) -> Result<Config> {
let mut reader = OpenOptions::new().read(true).open(path)?;
let mut reader = OpenOptions::new().read(true).open(&path)?;
let mut file = String::new();
reader.read_to_string(&mut file)?;
parse(&file)
@ -161,8 +96,8 @@ pub fn exists() -> bool {
}
/// Parses a phetch config file into a Config struct.
fn parse(text: &str) -> Result<Config> {
let mut cfg = Config::default();
pub fn parse(text: &str) -> Result<Config> {
let mut cfg = default();
let mut keys: HashMap<&str, bool> = HashMap::new();
for (mut linenum, line) in text.split_terminator('\n').enumerate() {
@ -173,17 +108,14 @@ fn parse(text: &str) -> Result<Config> {
}
// skip comments
if let Some('#') = line.chars().next() {
if let Some('#') = line.chars().nth(0) {
continue;
}
// line format: "KEY VALUE"
let parts: Vec<&str> = line.splitn(2, ' ').collect();
if parts.len() != 2 {
return Err(error!(
r#"Expected "key value" format on line {}: {:?}"#,
linenum, line
));
return Err(error!("Wrong format for line {}: {:?}", linenum, line));
}
let (key, val) = (parts[0], parts[1]);
if keys.contains_key(key) {
@ -195,74 +127,6 @@ fn parse(text: &str) -> Result<Config> {
"tls" => cfg.tls = to_bool(val)?,
"tor" => cfg.tor = to_bool(val)?,
"wide" => cfg.wide = to_bool(val)?,
"wrap" => {
if let Ok(num) = val.parse() {
cfg.wrap = num;
} else {
return Err(error!(
"`wrap` expects a number value on line {}: {}",
linenum, val
));
}
}
"scroll" => {
if let Ok(num) = val.parse() {
cfg.scroll = num;
} else {
return Err(error!(
"`scroll` expects a number value on line {}: {}",
linenum, val
));
}
}
"media" => {
cfg.media = match val.to_lowercase().as_ref() {
"false" | "none" => None,
_ => Some(val.into()),
}
}
"autoplay" => cfg.autoplay = to_bool(val)?,
"encoding" => {
cfg.encoding = Encoding::from_str(val)
.map_err(|e| error!("{} on line {}: {:?}", e, linenum, line))?;
}
"theme" => {
let homevar = std::env::var("HOME");
if homevar.is_err() && val.contains('~') {
return Err(error!("$HOME not set, can't decode `~`"));
}
cfg.theme = match load_file(&val.replace('~', &homevar.unwrap())) {
Ok(cfg) => cfg.theme,
Err(e) => {
if matches!(e.kind(), io::ErrorKind::NotFound) {
return Err(error!(
"error loading theme: File not found on line {}: {}",
linenum, val
));
} else {
return Err(error!("error loading theme: {:?}", e));
}
}
};
}
// color scheme
"ui.cursor" => cfg.theme.ui_cursor = to_color(val),
"ui.number" => cfg.theme.ui_number = to_color(val),
"ui.menu" => cfg.theme.ui_menu = to_color(val),
"ui.text" => cfg.theme.ui_text = to_color(val),
"item.text" => cfg.theme.item_text = to_color(val),
"item.menu" => cfg.theme.item_menu = to_color(val),
"item.error" => cfg.theme.item_error = to_color(val),
"item.search" => cfg.theme.item_search = to_color(val),
"item.telnet" => cfg.theme.item_telnet = to_color(val),
"item.external" => cfg.theme.item_external = to_color(val),
"item.download" => cfg.theme.item_download = to_color(val),
"item.media" => cfg.theme.item_media = to_color(val),
"item.unsupported" => cfg.theme.item_unsupported = to_color(val),
_ => return Err(error!("Unknown key on line {}: {}", linenum, key)),
}
keys.insert(key, true);
@ -293,7 +157,6 @@ mod tests {
assert_eq!(config.wide, false);
assert_eq!(config.emoji, false);
assert_eq!(config.start, "gopher://phetch/1/home");
assert_eq!(config.media, Some("mpv".to_string()));
}
#[test]
@ -322,21 +185,6 @@ mod tests {
assert_eq!(cfg.wide, true);
}
#[test]
fn test_media() {
let cfg = parse("media FALSE").unwrap();
assert_eq!(cfg.media, None);
let cfg = parse("media None").unwrap();
assert_eq!(cfg.media, None);
let cfg = parse("media /path/to/media-player").unwrap();
assert_eq!(cfg.media, Some("/path/to/media-player".to_string()));
let cfg = parse("media vlc").unwrap();
assert_eq!(cfg.media, Some("vlc".to_string()));
}
#[test]
fn test_no_or_false() {
let cfg = parse("tls false\nwide no\ntor n").unwrap();
@ -351,64 +199,4 @@ mod tests {
let e = res.unwrap_err();
assert_eq!(format!("{}", e), "Duplicate key on line 4: tls");
}
#[test]
fn test_encoding() {
let cfg = parse("tls true\nwide no\nemoji yes").unwrap();
assert_eq!(cfg.tls, true);
assert_eq!(cfg.encoding, Encoding::default());
let cfg = parse("tls true\nencoding utf8\n").unwrap();
assert_eq!(cfg.tls, true);
assert_eq!(cfg.encoding, Encoding::UTF8);
let cfg = parse("tls true\nencoding CP437\n").unwrap();
assert_eq!(cfg.tls, true);
assert_eq!(cfg.encoding, Encoding::CP437);
let res = parse("tls true\nencoding what\n");
assert!(res.is_err());
}
#[test]
fn test_missing_theme() {
if let Err(e) = parse("theme /dont/exists.txt") {
assert_eq!(
format!("{}", e),
"error loading theme: File not found on line 1: /dont/exists.txt"
);
}
}
#[test]
fn test_theme_file() {
use crate::theme::to_words;
let cfg = parse("theme ./tests/pink.theme").unwrap();
assert_eq!(to_words(cfg.theme.item_text), "magenta");
assert_eq!(to_words(cfg.theme.item_menu), "magenta");
assert_eq!(to_words(cfg.theme.item_error), "red");
assert_eq!(to_words(cfg.theme.ui_menu), "cyan");
}
#[test]
fn test_theme() {
use crate::theme::to_words;
let cfg = parse("item.text green\nitem.download red underline").unwrap();
assert_eq!(to_words(cfg.theme.item_text), "green");
assert_eq!(to_words(cfg.theme.item_download), "red underline");
}
#[test]
fn test_theme_bad_values() {
use crate::theme::to_words;
let cfg = parse("item.text skyblue\nitem.download green underline\nitem.error invisible\nitem.search red green blue")
.unwrap();
assert_eq!(to_words(cfg.theme.item_text), "white");
assert_eq!(to_words(cfg.theme.item_download), "green underline");
assert_eq!(to_words(cfg.theme.item_error), "white");
assert_eq!(to_words(cfg.theme.item_search), "red green blue");
}
}

@ -1,43 +0,0 @@
use std::{borrow::Cow, io::Result};
/// Encoding of Gopher response. Only UTF8 and CP437 are supported.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum Encoding {
/// Unicode
UTF8,
/// https://en.wikipedia.org/wiki/Code_page_437
CP437,
}
impl Default for Encoding {
fn default() -> Self {
Encoding::UTF8
}
}
impl Encoding {
/// Accepts a string like "UTF8" or "CP437" and returns the
/// appropriate `Encoding`, or an `Err`.
pub fn from_str(s: &str) -> Result<Self> {
match s.to_lowercase().as_ref() {
"utf8" | "utf-8" | "utf 8" => Ok(Encoding::UTF8),
"cp437" | "cp-437" | "cp 437" | "pc8" | "pc-8" | "oem us" | "oem-us" => {
Ok(Encoding::CP437)
}
_ => Err(error!("Expected CP437 or UTF8 encoding")),
}
}
/// Convert `response` into a String according to `encoding`.
pub fn encode<'res>(&self, response: &'res [u8]) -> Cow<'res, str> {
if matches!(self, Encoding::CP437) {
let mut converted = String::with_capacity(response.len());
for b in response {
converted.push_str(cp437::convert_byte(b));
}
Cow::from(converted)
} else {
String::from_utf8_lossy(response)
}
}
}

@ -4,7 +4,6 @@
//! URL parsing that recognizes different protocols like telnet and
//! IPv6 addresses.
use crate::ui::{self, Key};
use std::{
fs,
io::{Read, Result, Write},
@ -13,6 +12,7 @@ use std::{
os::unix::fs::OpenOptionsExt,
time::Duration,
};
use termion::input::TermRead;
#[cfg(feature = "tor")]
use tor_stream::TorStream;
@ -74,7 +74,7 @@ pub struct Url<'a> {
/// Fetches a gopher URL and returns a tuple of:
/// (did tls work?, raw Gopher response)
pub fn fetch_url(url: &str, tls: bool, tor: bool) -> Result<(bool, Vec<u8>)> {
pub fn fetch_url(url: &str, tls: bool, tor: bool) -> Result<(bool, String)> {
let u = parse_url(url);
fetch(u.host, u.port, u.sel, tls, tor)
}
@ -87,19 +87,13 @@ pub fn fetch(
selector: &str,
tls: bool,
tor: bool,
) -> Result<(bool, Vec<u8>)> {
) -> Result<(bool, String)> {
let mut stream = request(host, port, selector, tls, tor)?;
let mut body = Vec::new();
stream.read_to_end(&mut body)?;
Ok((stream.is_tls(), body))
}
/// Turn a Gopher response from `fetch` into a UTF8 String, cleaning
/// up unprintable characters along the way.
pub fn response_to_string(res: &[u8]) -> String {
let mut s = String::from_utf8_lossy(res).to_string();
clean_response(&mut s);
s
let mut out = String::from_utf8_lossy(&body).to_string();
clean_response(&mut out);
Ok((stream.is_tls(), out))
}
/// Removes unprintable characters from Gopher response.
@ -107,75 +101,26 @@ pub fn response_to_string(res: &[u8]) -> String {
fn clean_response(res: &mut String) {
res.retain(|c| match c {
'\u{007F}' => false,
_ if ('\u{0080}'..='\u{009F}').contains(&c) => false,
_ if c >= '\u{0080}' && c <= '\u{009F}' => false,
_ => true,
})
}
/// Downloads menu or text to disk as `filename`.
/// Allows canceling with Ctrl-c, but it's
/// kind of hacky - needs the UI receiver passed in.
/// Returns a tuple of:
/// (path it was saved to, the size in bytes)
pub fn download_url_with_filename(
url: &str,
tls: bool,
tor: bool,
chan: ui::KeyReceiver,
filename: &str,
) -> Result<(String, usize)> {
let u = parse_url(url);
let mut path = std::path::PathBuf::from(".");
path.push(filename);
let mut stream = request(u.host, u.port, u.sel, tls, tor)?;
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.append(true)
.open(&path)
.map_err(|e| error!("{}", e))?;
let mut buf = [0; 1024];
let mut bytes = 0;
while let Ok(count) = stream.read(&mut buf) {
if count == 0 {
break;
}
bytes += count;
file.write_all(&buf[..count])?;
if let Ok(chan) = chan.lock() {
if let Ok(Key::Ctrl('c')) = chan.try_recv() {
if path.exists() {
fs::remove_file(path)?;
}
return Err(error!("Download cancelled"));
}
}
}
Ok((filename.to_string(), bytes))
}
/// Downloads a binary to disk. Allows canceling with Ctrl-c, but it's
/// kind of hacky - needs the UI receiver passed in.
/// Downloads a binary to disk. Allows canceling with Ctrl-c.
/// Returns a tuple of:
/// (path it was saved to, the size in bytes)
pub fn download_url(
url: &str,
tls: bool,
tor: bool,
chan: ui::KeyReceiver,
) -> Result<(String, usize)> {
pub fn download_url(url: &str, tls: bool, tor: bool) -> Result<(String, usize)> {
let u = parse_url(url);
let filename = u
.sel
.split_terminator('/')
.rev()
.next()
.nth(0)
.ok_or_else(|| error!("Bad download filename: {}", u.sel))?;
let mut path = std::path::PathBuf::from(".");
path.push(filename);
let stdin = termion::async_stdin();
let mut keys = stdin.keys();
let mut stream = request(u.host, u.port, u.sel, tls, tor)?;
let mut file = fs::OpenOptions::new()
@ -193,16 +138,13 @@ pub fn download_url(
}
bytes += count;
file.write_all(&buf[..count])?;
if let Ok(chan) = chan.lock() {
if let Ok(Key::Ctrl('c')) = chan.try_recv() {
if path.exists() {
fs::remove_file(path)?;
}
return Err(error!("Download cancelled"));
if let Some(Ok(termion::event::Key::Ctrl('c'))) = keys.next() {
if path.exists() {
fs::remove_file(path)?;
}
return Err(error!("Download cancelled"));
}
}
Ok((filename.to_string(), bytes))
}
@ -219,10 +161,10 @@ pub fn request(host: &str, port: &str, selector: &str, tls: bool, tor: bool) ->
{
{
if let Ok(connector) = TlsConnector::new() {
let stream = addr
.to_socket_addrs()?
.find_map(|s| TcpStream::connect_timeout(&s, TCP_TIMEOUT_DURATION).ok())
.ok_or_else(|| error!("Can't create socket"))?;
let sock = addr.to_socket_addrs().and_then(|mut socks| {
socks.next().ok_or_else(|| error!("Can't create socket"))
})?;
let stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?;
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION))?;
if let Ok(mut stream) = connector.connect(host, stream) {
stream.write_all(selector.as_ref())?;
@ -241,11 +183,15 @@ pub fn request(host: &str, port: &str, selector: &str, tls: bool, tor: bool) ->
if tor {
#[cfg(feature = "tor")]
{
let mut stream = std::env::var("TOR_PROXY")
let proxy = std::env::var("TOR_PROXY")
.unwrap_or_else(|_| "127.0.0.1:9050".into())
.to_socket_addrs()?
.find_map(|s| TorStream::connect_with_address(s, addr.as_ref()).ok())
.ok_or_else(|| error!("Can't create socket"))?;
.nth(0)
.unwrap();
let mut stream = match TorStream::connect_with_address(proxy, addr.as_ref()) {
Ok(s) => s,
Err(e) => return Err(error!("Tor error: {}", e)),
};
stream.write_all(selector.as_ref())?;
stream.write_all("\r\n".as_ref())?;
return Ok(Stream {
@ -256,10 +202,10 @@ pub fn request(host: &str, port: &str, selector: &str, tls: bool, tor: bool) ->
}
// no tls or tor, try regular connection
let mut stream = addr
.to_socket_addrs()?
.find_map(|s| TcpStream::connect_timeout(&s, TCP_TIMEOUT_DURATION).ok())
.ok_or_else(|| error!("Can't create socket"))?;
let sock = addr
.to_socket_addrs()
.and_then(|mut socks| socks.next().ok_or_else(|| error!("Can't create socket")))?;
let mut stream = TcpStream::connect_timeout(&sock, TCP_TIMEOUT_DURATION)?;
stream.set_read_timeout(Some(TCP_TIMEOUT_DURATION))?;
stream.write_all(selector.as_ref())?;
stream.write_all("\r\n".as_ref())?;
@ -328,7 +274,7 @@ pub fn parse_url(url: &str) -> Url {
host = &url[..idx];
sel = &url[idx..];
} else {
host = url;
host = &url;
}
// ipv6
@ -355,7 +301,7 @@ pub fn parse_url(url: &str) -> Url {
// ignore type prefix on selector
if typ != Type::Telnet {
let mut chars = sel.chars();
if let (Some('/'), Some(c)) = (chars.next(), chars.next()) {
if let (Some('/'), Some(c)) = (chars.nth(0), chars.nth(0)) {
if let Some(t) = Type::from(c) {
typ = t;
sel = &sel[2..];
@ -388,149 +334,97 @@ mod tests {
"ssh://kiosk@bitreich.org",
"https://github.com/xvxx/phetch",
"telnet://bbs.impakt.net:6502/",
"gopher://some.url/9/file.mp4",
"gopher://some.url/;/file.mp4",
"mtv.com/s/best-of-britney-spears.mp3",
"gopher://microsoft.com:7070/x/developer/sitemap.xml",
"gopher://mtv.com/c/kriss-kross/tour-dates.ical",
"gopher://protonmail.com/M/mymail/inbox.eml",
];
let mut urls = urls.iter();
macro_rules! parse_next_url {
() => {
parse_url(urls.next().unwrap())
};
}
let url = parse_next_url!();
let url = parse_url(urls[0]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "gopher.club");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/phlogs/");
let url = parse_next_url!();
let url = parse_url(urls[1]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "sdf.org");
assert_eq!(url.port, "7777");
assert_eq!(url.sel, "/maps");
let url = parse_next_url!();
let url = parse_url(urls[2]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "gopher.floodgap.org");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[3]);
assert_eq!(url.typ, Type::Text);
assert_eq!(url.host, "gopher.floodgap.com");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/gopher/relevance.txt");
let url = parse_next_url!();
let url = parse_url(urls[4]);
assert_eq!(url.typ, Type::Search);
assert_eq!(url.host, "gopherpedia.com");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/lookup?Gopher");
let url = parse_next_url!();
let url = parse_url(urls[5]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "dead:beef:1234:5678:9012:3456:feed:deed");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[6]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "1234:2345:dead:4567:7890:1234:beef:1111");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/files");
let url = parse_next_url!();
let url = parse_url(urls[7]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "2001:cdba:0000:0000:0000:0000:3257:9121");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[8]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "2001:cdba::3257:9652");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[9]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "9999:aaaa::abab:baba:aaaa:9999");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[10]);
assert_eq!(url.typ, Type::Error);
assert_eq!(url.host, "Unclosed ipv6 bracket");
assert_eq!(url.port, "");
assert_eq!(url.sel, "[2001:2099:dead:beef:0000");
let url = parse_next_url!();
let url = parse_url(urls[11]);
assert_eq!(url.typ, Type::Menu);
assert_eq!(url.host, "::1");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "");
let url = parse_next_url!();
let url = parse_url(urls[12]);
assert_eq!(url.typ, Type::HTML);
assert_eq!(url.host, "");
assert_eq!(url.port, "");
assert_eq!(url.sel, "ssh://kiosk@bitreich.org");
let url = parse_next_url!();
let url = parse_url(urls[13]);
assert_eq!(url.typ, Type::HTML);
assert_eq!(url.host, "");
assert_eq!(url.port, "");
assert_eq!(url.sel, "https://github.com/xvxx/phetch");
let url = parse_next_url!();
let url = parse_url(urls[14]);
assert_eq!(url.typ, Type::Telnet);
assert_eq!(url.host, "bbs.impakt.net");
assert_eq!(url.port, "6502");
assert_eq!(url.sel, "/");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Binary);
assert_eq!(url.host, "some.url");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/file.mp4");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Video);
assert_eq!(url.host, "some.url");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/file.mp4");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Sound);
assert_eq!(url.host, "mtv.com");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/best-of-britney-spears.mp3");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Xml);
assert_eq!(url.host, "microsoft.com");
assert_eq!(url.port, "7070");
assert_eq!(url.sel, "/developer/sitemap.xml");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Calendar);
assert_eq!(url.host, "mtv.com");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/kriss-kross/tour-dates.ical");
let url = parse_next_url!();
assert_eq!(url.typ, Type::Mailbox);
assert_eq!(url.host, "protonmail.com");
assert_eq!(url.port, "70");
assert_eq!(url.sel, "/mymail/inbox.eml");
// make sure we got em all
assert_eq!(urls.next(), None);
}
#[test]

@ -2,7 +2,7 @@ use std::fmt;
/// Gopher types are defined according to RFC 1436.
#[allow(missing_docs)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Type {
Text, // 0 | cyan
Menu, // 1 | blue
@ -21,12 +21,8 @@ pub enum Type {
Image, // I | download
PNG, // p | download
Info, // i | yellow
Sound, // s | green underline
Sound, // s | download
Document, // d | download
Video, // ; | green underline
Xml, // x | cyan
Calendar, // c | download
Mailbox, // M | unsupported
}
impl Type {
@ -37,7 +33,7 @@ impl Type {
/// Text document?
pub fn is_text(self) -> bool {
matches!(self, Type::Text | Type::Xml)
self == Type::Text
}
/// HTML link?
@ -57,38 +53,31 @@ impl Type {
/// Is this something we can download?
pub fn is_download(self) -> bool {
matches!(
self,
match self {
Type::Binhex
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
| Type::Image
| Type::PNG
| Type::Sound
| Type::Video
| Type::Calendar
| Type::Document
)
}
/// Check if media to open in player
pub fn is_media(self) -> bool {
matches!(self, Type::Sound | Type::Video)
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
| Type::Image
| Type::PNG
| Type::Sound
| Type::Document => true,
_ => false,
}
}
/// Is this a type phetch supports?
pub fn is_supported(self) -> bool {
!matches!(
self,
Type::CSOEntity | Type::Mirror | Type::Telnet3270 | Type::Mailbox
)
match self {
Type::CSOEntity | Type::Mirror | Type::Telnet3270 => false,
_ => true,
}
}
/// Gopher Item Type to RFC char.
pub fn to_char(self) -> char {
match self {
pub fn to_char(self) -> Option<char> {
Some(match self {
Type::Text => '0',
Type::Menu => '1',
Type::CSOEntity => '2',
@ -108,11 +97,7 @@ impl Type {
Type::Info => 'i',
Type::Sound => 's',
Type::Document => 'd',
Type::Video => ';',
Type::Calendar => 'c',
Type::Xml => 'x',
Type::Mailbox => 'M',
}
})
}
/// Create a Gopher Item Type from its RFC char code.
@ -137,10 +122,6 @@ impl Type {
'i' => Type::Info,
's' => Type::Sound,
'd' => Type::Document,
';' => Type::Video,
'c' => Type::Calendar,
'x' => Type::Xml,
'M' => Type::Mailbox,
_ => return None,
})
}
@ -148,6 +129,10 @@ impl Type {
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.to_char())
if let Some(c) = self.to_char() {
write!(f, "{}", c)
} else {
write!(f, "?")
}
}
}

@ -10,7 +10,6 @@ pub fn lookup(name: &str) -> Option<String> {
"history" => history::as_raw_menu(),
"bookmarks" => bookmarks::as_raw_menu(),
"help/config" => format!("{}{}", HEADER, CONFIG),
"help/themes" => format!("{}{}", HEADER, THEMES),
"help/keys" => format!("{}{}", HEADER, KEYS),
"help/nav" => format!("{}{}", HEADER, NAV),
"help/types" => format!("{}{}", HEADER, TYPES),
@ -85,7 +84,6 @@ i
1bookmarks /help/bookmarks phetch
1history /help/history phetch
1phetch.conf /help/config phetch
1themes /help/themes phetch
i
i ~ * ~
i
@ -123,9 +121,7 @@ is save bookmark
ia show history
i
ir view raw source
id download raw source
iw toggle wide mode
ie toggle encoding
iq quit phetch
ih show help
i
@ -215,14 +211,14 @@ i ** config **
i
iif you create a phetch.conf
ifile in ~/.config/phetch/ it
iwill be automatically loaded
iwill be automatically loaded
iwhen phetch starts. the config
ifile supports most command line
ioptions, for your convenience.
i
ifor example, phetch will always
ilaunch in TLS mode if `tls yes`
iappears in the config file.
iappears in the config file.
i
ihere is an example phetch.conf
iwith all possible keys:
@ -241,84 +237,6 @@ iwide no
i
i# show emoji status indicators
iemoji no
i
i# cp437 or utf8 encoding
iencoding utf8
i
i# wrap text at N cols. 0 = off
iwrap 0
i
i# page up/down by N lines.
i# 0 = full screen
iscroll 0
i
i# path to theme file, if any
itheme ~/.config/phetch/fun.theme
";
const THEMES: &str = "
i ** themes **
i
iyou can change phetch's color
ischeme by supplying your own
itheme file with --theme/-t or
iby setting `theme FILE` in
iyour phetch.conf.
i
iyou can also set colors directly
iin your phetch.conf.
i
iview the current theme with:
i
i$ phetch --print-theme
i
i ** examples **
i
itheme files are plain text files
ithat look like this:
i
iui.cursor white bold
iui.number magenta
iui.menu yellow
iui.text white
iitem.text cyan
iitem.menu blue
iitem.error red
iitem.search white
iitem.telnet grey
iitem.external green
iitem.download white underline
iitem.media green underline
iitem.unsupported whitebg red
i
i ** valid colors **
i
ibold
iunderline
igrey
ired
igreen
iyellow
iblue
imagenta
icyan
iwhite
iblack
idarkred
idarkgreen
idarkyellow
idarkblue
idarkmagenta
idarkcyan
idarkwhite
iblackbg
iredbg
igreenbg
iyellowbg
ibluebg
imagentabg
icyanbg
iwhitebg
";
const TYPES: &str = "
@ -333,7 +251,7 @@ i
8telnet links /help/types phetch
hexternal urls URL:https://en.wikipedia.org/wiki/Phetch phetch
i
ithese download types:
iand these download types:
i
4binhex /help/types phetch
5dosfiles /help/types phetch
@ -341,13 +259,9 @@ i
9binaries /help/types phetch
gGIFs /help/types phetch
Iimages downloads /help/types phetch
ssound files /help/types phetch
ddocuments /help/types phetch
i
iand these media types:
i
ssound files URL:https://freepd.com/music/Wakka%20Wakka.mp3 phetch
;video files URL:https://www.youtube.com/watch?v=oHg5SJYRHA0 phetch
i
iphetch does not support:
i
2CSO Entries /help/types phetch
@ -367,15 +281,15 @@ i ~ * ~
i
i special thanks
i
ikseistrup:
ikseistrup:
i major design, testing,
i documentation help
i
iantirez:
iantirez:
i added gopher to redis
i and opened the door
i
ilartu:
ilartu:
i inspired me to add some
i \x1b[95mcolor\x1b[0m
i

@ -33,17 +33,13 @@
#![allow(clippy::while_let_on_iterator)]
#![allow(clippy::write_with_newline)]
#[macro_use]
extern crate lazy_static;
#[macro_use]
pub mod utils;
#[macro_use]
pub mod theme;
pub mod color;
pub mod args;
pub mod bookmarks;
pub mod config;
mod encoding;
pub mod gopher;
pub mod help;
pub mod history;
@ -78,8 +74,3 @@ pub const TOR_SUPPORT: bool = true;
#[cfg(not(feature = "tor"))]
/// Whether we compiled with Tor support.
pub const TOR_SUPPORT: bool = false;
lazy_static! {
/// Is the NO_COLOR env variable set? https://no-color.org/
pub static ref NO_COLOR: bool = std::env::var("NO_COLOR").is_ok();
}

@ -1,34 +1,29 @@
use phetch::{
args,
config::{Config, SharedConfig},
gopher, menu, terminal, theme,
args, gopher, menu,
ui::{Mode, UI},
};
use std::{
env,
error::Error,
io::{self, stdout, Write},
panic, process, str,
};
use std::{env, process};
fn main() {
if let Err(e) = run() {
eprintln!("{}", e);
process::exit(1);
}
process::exit(run())
}
/// Start the app. Returns UNIX exit code.
fn run() -> Result<(), Box<dyn Error>> {
fn run() -> i32 {
let str_args = env::args().skip(1).collect::<Vec<String>>();
let mut cfg = args::parse(&str_args)?;
let mut cfg = match args::parse(&str_args) {
Ok(c) => c,
Err(e) => {
eprintln!("{}", e);
return 1;
}
};
// check for simple modes
match cfg.mode {
Mode::Raw => return print_raw(&cfg.start, cfg.tls, cfg.tor),
Mode::Version => return print_version(),
Mode::Help => return print_usage(),
Mode::PrintTheme => return print_theme(cfg),
Mode::NoTTY => return print_plain(&cfg.start, cfg.tls, cfg.tor),
Mode::Print => cfg.wide = true,
Mode::Run => {}
@ -38,35 +33,48 @@ fn run() -> Result<(), Box<dyn Error>> {
let start = cfg.start.clone();
let mode = cfg.mode;
let mut ui = UI::new(cfg);
ui.open(&start, &start)?;
if let Err(e) = ui.open(&start, &start) {
eprintln!("{}", e);
return 1;
}
// print rendered version
if mode == Mode::Print {
println!("{}", ui.render()?);
return Ok(());
return match ui.render() {
Ok(screen) => {
println!("{}", screen);
0
}
Err(e) => {
eprintln!("{}", e);
1
}
};
}
// run app
setup_terminal();
ui.run()?;
cleanup_terminal();
if let Err(e) = ui.run() {
eprintln!("{}", e);
return 1;
}
Ok(())
// and scene
0
}
/// --version
fn print_version() -> Result<(), Box<dyn Error>> {
fn print_version() -> i32 {
println!(
"phetch v{version} ({built})",
built = phetch::BUILD_DATE,
version = phetch::VERSION
);
Ok(())
0
}
/// --help
fn print_usage() -> Result<(), Box<dyn Error>> {
print_version()?;
fn print_usage() -> i32 {
print_version();
println!(
"
Usage:
@ -79,24 +87,14 @@ Options:
-s, --tls Try to open Gopher URLs securely w/ TLS
-o, --tor Use local Tor proxy to open all pages
-S, -O Disable TLS or Tor
-w, --wrap COLUMN Wrap long lines in \"text\" views at COLUMN.
-m, --media PROGRAM Use to open media files. Default: mpv
-M, --no-media Just download media files, don't download
-a, --autoplay Autoplay media without prompting.
-A, --no-autoplay Prompt before playing media.
-r, --raw Print raw Gopher response only
-p, --print Print rendered Gopher response only
-l, --local Connect to 127.0.0.1:7070
-e, --encoding Render text documents in CP437 or UTF8.
-c, --config FILE Use instead of ~/.config/phetch/phetch.conf
-C, --no-config Don't use any config file
-t, --theme FILE Use FILE for color theme or print current theme.
--print-theme Print current theme.
-C, --no-config Don't use any config file
-h, --help Show this screen
-v, --version Show phetch version
@ -104,76 +102,48 @@ Command line options always override options set in phetch.conf.
Once you've launched phetch, use `ctrl-h` to view the on-line help."
);
Ok(())
0
}
/// Print just the raw Gopher response.
fn print_raw(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
let (_, out) = gopher::fetch_url(url, tls, tor)?;
println!("{}", gopher::response_to_string(&out));
Ok(())
fn print_raw(url: &str, tls: bool, tor: bool) -> i32 {
match gopher::fetch_url(url, tls, tor) {
Ok((_, response)) => {
println!("{}", response);
0
}
Err(e) => {
eprintln!("{}", e);
1
}
}
}
/// Print a colorless, plain version of the response for a non-tty
/// (like a pipe).
fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
fn print_plain(url: &str, tls: bool, tor: bool) -> i32 {
let mut out = String::new();
let typ = gopher::type_for_url(url);
let (_, response) = gopher::fetch_url(url, tls, tor)?;
let response = gopher::response_to_string(&response);
match typ {
gopher::Type::Menu => {
let menu = menu::parse(url, response, SharedConfig::default());
for line in menu.lines() {
out.push_str(line.text());
out.push('\n');
match gopher::fetch_url(url, tls, tor) {
Ok((_, response)) => match typ {
gopher::Type::Menu => {
let menu = menu::parse(url, response);
for line in menu.lines {
out.push_str(line.text(&menu.raw));
out.push('\n');
}
}
gopher::Type::Text => println!("{}", response.trim_end_matches(".\r\n")),
_ => {
eprintln!("can't print gopher type: {:?}", typ);
return 1;
}
},
Err(e) => {
eprintln!("{}", e);
return 1;
}
gopher::Type::Text => println!("{}", response.trim_end_matches(".\r\n")),
_ => {
return Err(Box::new(io::Error::new(
io::ErrorKind::Other,
format!("can't print gopher type: {:?}", typ),
)));
}
};
}
print!("{}", out);
Ok(())
}
/// Print current theme as plaintext
fn print_theme(cfg: Config) -> Result<(), Box<dyn Error>> {
println!("{}", cfg.theme);
Ok(())
}
/// Put the terminal into raw mode, enter the alternate screen, and
/// setup the panic handler.
fn setup_terminal() {
let old_handler = panic::take_hook();
panic::set_hook(Box::new(move |info| {
cleanup_terminal();
old_handler(info);
}));
terminal::enable_raw_mode().expect("Fatal Error entering Raw Mode.");
write!(stdout(), "{}", terminal::ToAlternateScreen)
.expect("Fatal Error entering Alternate Mode.");
}
/// Leave raw mode. Need to always do this, even on panic.
fn cleanup_terminal() {
let mut stdout = stdout();
write!(
stdout,
"{}{}{}{}{}",
theme::color::Reset,
terminal::ClearAll,
terminal::Goto(1, 1),
terminal::ShowCursor,
terminal::ToMainScreen
)
.expect("Fatal Error cleaning up terminal.");
stdout.flush().expect("Fatal Error cleaning up terminal.");
terminal::disable_raw_mode().expect("Fatal Error leaving Raw Mode.");
0
}

@ -7,10 +7,10 @@
//! it returns an Action to the UI representing its intent.
use crate::{
config::SharedConfig as Config,
config::Config,
gopher::{self, Type},
terminal,
ui::{self, Action, Key, View, MAX_COLS},
ui::{self, Action, Key, View, MAX_COLS, SCROLL_LINES},
};
use std::fmt;
@ -23,9 +23,8 @@ use std::fmt;
pub struct Menu {
/// Gopher URL
pub url: String,
/// Lines in the menu. Not all are links. Use the `lines()` iter
/// or `line(N)` or `link(N)` to access one.
spans: Vec<LineSpan>,
/// Lines in the menu. Not all are links.
pub lines: Vec<Line>,
/// Indexes of links in the `lines` vector. Pauper's pointers.
pub links: Vec<usize>,
/// Currently selected link. Index of the `links` vec.
@ -38,8 +37,8 @@ pub struct Menu {
pub input: String,
/// UI mode. Interactive (Run), Printing, Raw mode...
pub mode: ui::Mode,
/// Scrolling offset, in rows. 0 = full screen
pub offset: usize,
/// Scrolling offset, in rows.
pub scroll: usize,
/// Incremental search mode?
pub searching: bool,
/// Was this menu retrieved via TLS?
@ -50,47 +49,55 @@ pub struct Menu {
pub size: (usize, usize),
/// Wide mode?
wide: bool,
/// Scroll by how many lines?
scroll: usize,
/// Global config
config: Config,
}
/// Represents a line in a Gopher menu. Provides the actual text of
/// the line, vs LineSpan which is just location data.
pub struct Line<'line, 'txt: 'line> {
span: &'line LineSpan,
text: &'txt str,
/// The Line represents a single line in a Gopher menu.
/// It must exist in the context of a Menu struct - its `link`
/// field is its index in the Menu's `links` Vec, and
/// start/end/text_end point to locations in Menu's `raw` Gopher
/// response.
pub struct Line {
/// Gopher Item Type.
pub typ: Type,
/// Where this line starts in its Menu's `raw` Gopher response.
start: usize,
/// Where this line ends in Menu.raw.
end: usize,
/// Where the text/label of this line ends. Might be the same as
/// `end`, or might be earlier.
text_end: usize,
/// Index of this link in the Menu::links vector, if it's a
/// `gopher::Type.is_link()`
pub link: usize,
}
impl<'line, 'txt> Line<'line, 'txt> {
fn new(span: &'line LineSpan, text: &'txt str) -> Line<'line, 'txt> {
Line { span, text }
}
/// Visible line as text. What appeared in the raw Gopher
/// response.
pub fn text(&self) -> &str {
impl Line {
/// Returns the text field of this line, given a raw Gopher response.
/// The same Line must always be used with the same Gopher response.
pub fn text<'a>(&self, raw: &'a str) -> &'a str {
if self.start < self.text_end {
&self.text[self.start + 1..self.text_end]
&raw[self.start + 1..self.text_end]
} else {
""
}
}
/// Truncated version of the line, according to visible characters
/// and MAX_COLS.
pub fn text_truncated(&self) -> String {
self.text().chars().take(self.truncated_len).collect()
/// Get the length of this line's text field.
pub fn text_len(&self) -> usize {
if self.text_end > self.start {
self.text_end - self.start
} else {
0
}
}
/// URL for this line, if it's a link.
pub fn url(&self) -> String {
/// Get the URL for this line, if it's a link.
pub fn url(&self, raw: &str) -> String {
if !self.typ.is_link() || self.text_end >= self.end {
return String::from("");
}
let line = &self.text[self.text_end..self.end].trim_end_matches('\r');
let line = &raw[self.text_end..self.end].trim_end_matches('\r');
let mut sel = "(null)";
let mut host = "localhost";
let mut port = "70";
@ -124,80 +131,6 @@ impl<'line, 'txt> Line<'line, 'txt> {
}
}
/// Line wraps LineSpan.
impl<'line, 'txt: 'line> std::ops::Deref for Line<'line, 'txt> {
type Target = LineSpan;
fn deref(&self) -> &Self::Target {
self.span
}
}
/// The LineSpan represents a single line's location in a Gopher menu.
/// It only exists in the context of a Menu struct - its `link`
/// field is its index in the Menu's `links` Vec, and
/// start/end/text_end point to locations in Menu's `raw` Gopher
/// response.
/// You won't really interact with this directly, instead call
/// `menu.lines()` get an iter over `Line` or `menu.line(idx)` to get
/// a single Line.
pub struct LineSpan {
/// Gopher Item Type.
pub typ: Type,
/// Where this line starts in its Menu's `raw` Gopher response.
start: usize,
/// Where this line ends in Menu.raw.
end: usize,
/// Where the text/label of this line ends. Might be the same as
/// `end`, or might be earlier.
text_end: usize,
/// Length of visible text, ignoring ANSI escape codes (colors).
visible_len: usize,
/// How many chars() to grab from text() if we want to only show
/// `MAX_COLS` visible chars on screen, aka ignore ANSI escape
/// codes and colors.
truncated_len: usize,
/// Index of this link in the Menu::links vector, if it's a
/// `gopher::Type.is_link()`
pub link: usize,
}
impl LineSpan {
/// Get the length of this line's text field.
pub fn text_len(&self) -> usize {
self.visible_len
}
}
/// Iterator over (dynamically created) Line structs.
pub struct LinesIter<'menu> {
spans: &'menu [LineSpan],
text: &'menu str,
curr: usize,
}
impl<'menu> LinesIter<'menu> {
fn new(spans: &'menu [LineSpan], text: &'menu str) -> LinesIter<'menu> {
LinesIter {
spans,
text,
curr: 0,
}
}
}
impl<'menu> Iterator for LinesIter<'menu> {
type Item = Line<'menu, 'menu>;
fn next(&mut self) -> Option<Self::Item> {
if self.curr >= self.spans.len() {
None
} else {
let line_with = Line::new(&self.spans[self.curr], self.text);
self.curr += 1;
Some(line_with)
}
}
}
/// Direction of a given link relative to the visible screen.
#[derive(PartialEq)]
enum LinkPos {
@ -253,37 +186,16 @@ impl View for Menu {
impl Menu {
/// Create a representation of a Gopher Menu from a raw Gopher
/// response and a few options.
pub fn from(url: &str, response: String, config: Config, tls: bool) -> Menu {
pub fn from(url: &str, response: String, config: &Config, tls: bool) -> Menu {
Menu {
tls,
tor: config.read().unwrap().tor,
wide: config.read().unwrap().wide,
scroll: config.read().unwrap().scroll,
mode: config.read().unwrap().mode,
..parse(url, response, config.clone())
}
}
/// Lines in this menu. Main iterator for getting Line with text.
pub fn lines(&self) -> LinesIter {
LinesIter::new(&self.spans, &self.raw)
}
/// Get a single Line in this menu by index.
pub fn line(&self, idx: usize) -> Option<Line> {
if idx >= self.spans.len() {
None
} else {
Some(Line::new(&self.spans[idx], &self.raw))
tor: config.tor,
wide: config.wide,
mode: config.mode,
..parse(url, response)
}
}
/// Find a link by its link index.
pub fn link(&self, idx: usize) -> Option<Line> {
let line = self.links.get(idx)?;
self.line(*line)
}
fn cols(&self) -> usize {
self.size.0
}
@ -292,14 +204,6 @@ impl Menu {
self.size.1
}
fn scroll_by(&self) -> usize {
if self.scroll == 0 {
self.rows() - 1
} else {
self.scroll
}
}
/// Calculated size of left margin.
fn indent(&self) -> usize {
if self.wide {
@ -323,6 +227,12 @@ impl Menu {
}
}
/// Find a link by its link index.
fn link(&self, i: usize) -> Option<&Line> {
let line = self.links.get(i)?;
self.lines.get(*line)
}
/// Is the given link visible on screen?
fn is_visible(&self, link: usize) -> bool {
self.link_visibility(link) == Some(LinkPos::Visible)
@ -331,9 +241,9 @@ impl Menu {
/// Where is the given link relative to the screen?
fn link_visibility(&self, i: usize) -> Option<LinkPos> {
let &pos = self.links.get(i)?;
Some(if pos < self.offset {
Some(if pos < self.scroll {
LinkPos::Above
} else if pos >= self.offset + self.rows() - 1 {
} else if pos >= self.scroll + self.rows() - 1 {
LinkPos::Below
} else {
LinkPos::Visible
@ -347,10 +257,10 @@ impl Menu {
}
let &pos = self.links.get(link)?;
let x = self.indent() + 1;
let y = if self.offset > pos {
let y = if self.scroll > pos {
pos + 1
} else {
pos + 1 - self.offset
pos + 1 - self.scroll
};
Some((x as u16, y as u16))
@ -363,61 +273,64 @@ impl Menu {
// (status bar is always last line)
self.rows() - 1
} else {
self.spans.len()
self.lines.len()
};
let iter = self.lines().skip(self.offset).take(limit);
let iter = self.lines.iter().skip(self.scroll).take(limit);
let indent = self.indent();
let left_margin = " ".repeat(indent);
for line in iter {
out.push_str(&left_margin);
let config = self.config.read().unwrap();
if line.typ == Type::Info {
out.push_str(" ");
} else {
if line.link == self.link && self.show_cursor() {
out.push_str(&config.theme.ui_cursor);
out.push_str(color!(Bold));
out.push('*');
out.push_str(reset_color!());
out.push_str(color!(Reset));
} else {
out.push(' ');
}
out.push(' ');
out.push_str(&config.theme.ui_number);
out.push_str(color!(Magenta));
if line.link < 9 {
out.push(' ');
}
let num = (line.link + 1).to_string();
out.push_str(&num);
out.push_str(". ");
out.push_str(reset_color!());
out.push_str(color!(Reset));
}
// truncate long lines, instead of wrapping
let text = line.text_truncated();
let text = if line.text_len() > MAX_COLS {
&line.text(&self.raw)[..MAX_COLS]
} else {
&line.text(&self.raw)
};
// color the line
if line.typ.is_media() {
out.push_str(&config.theme.item_media);
} else if line.typ.is_download() {
out.push_str(&config.theme.item_download);
if line.typ.is_download() {
out.push_str(color!(Underline));
out.push_str(color!(White));
} else if !line.typ.is_supported() {
out.push_str(&self.config.read().unwrap().theme.item_unsupported);
out.push_str(color!(WhiteBG));
out.push_str(color!(Red));
} else {
out.push_str(match line.typ {
Type::Text => &config.theme.item_text,
Type::Menu => &config.theme.item_menu,
Type::Info => &config.theme.ui_menu,
Type::HTML => &config.theme.item_external,
Type::Error => &config.theme.item_error,
Type::Telnet => &config.theme.item_telnet,
Type::Search => &config.theme.item_search,
_ => &config.theme.item_error,
out.push_str(&match line.typ {
Type::Text => color!(Cyan),
Type::Menu => color!(Blue),
Type::Info => color!(Yellow),
Type::HTML => color!(Green),
Type::Error => color!(Red),
Type::Telnet => color!(Grey),
Type::Search => color!(White),
_ => color!(Red),
});
}
out.push_str(&text);
out.push_str(reset_color!());
out.push_str(color!(Reset));
// clear rest of line
out.push_str(terminal::ClearUntilNewline.as_ref());
@ -462,9 +375,8 @@ impl Menu {
}
let (x, y) = self.screen_coords(self.link)?;
Some(format!(
"{}{}*\x1b[0m{}",
"{}\x1b[97;1m*\x1b[0m{}",
terminal::Goto(x, y),
self.config.read().unwrap().theme.ui_cursor,
terminal::HideCursor
))
}
@ -487,11 +399,11 @@ impl Menu {
}
}
/// Scroll down by a page, if possible.
/// Scroll down by SCROLL_LINES, if possible.
fn action_page_down(&mut self) -> Action {
// If there are fewer menu items than screen lines, just
// select the final link and do nothing else.
if self.spans.len() < self.rows() {
if self.lines.len() < self.rows() {
if !self.links.is_empty() {
self.link = self.links.len() - 1;
return Action::Redraw;
@ -501,8 +413,8 @@ impl Menu {
// If we've already scrolled too far, select the final link
// and do nothing.
if self.offset >= self.final_offset() {
self.offset = self.final_offset();
if self.scroll >= self.final_scroll() {
self.scroll = self.final_scroll();
if !self.links.is_empty() {
self.link = self.links.len() - 1;
}
@ -510,11 +422,11 @@ impl Menu {
}
// Scroll...
self.offset += self.scroll_by();
self.scroll += SCROLL_LINES;
// ...but don't go past the final line.
if self.offset > self.final_offset() {
self.offset = self.final_offset();
if self.scroll > self.final_scroll() {
self.scroll = self.final_scroll();
}
// If the selected link isn't visible...
@ -524,9 +436,9 @@ impl Menu {
.links
.iter()
.skip(self.link + 1)
.find(|&&i| i >= self.offset)
.find(|&&i| i >= self.scroll)
{
if let Some(next_link_line) = self.line(next_link_pos) {
if let Some(next_link_line) = self.lines.get(next_link_pos) {
self.link = next_link_line.link;
}
}
@ -536,11 +448,11 @@ impl Menu {
}
fn action_page_up(&mut self) -> Action {
if self.offset > 0 {
if self.offset > self.scroll_by() {
self.offset -= self.scroll_by();
if self.scroll > 0 {
if self.scroll > SCROLL_LINES {
self.scroll -= SCROLL_LINES;
} else {
self.offset = 0;
self.scroll = 0;
}
if self.link == 0 {
return Action::Redraw;
@ -548,7 +460,7 @@ impl Menu {
if let Some(dir) = self.link_visibility(self.link) {
match dir {
LinkPos::Below => {
let scroll = self.offset;
let scroll = self.scroll;
if let Some(&pos) = self
.links
.iter()
@ -556,7 +468,7 @@ impl Menu {
.rev()
.find(|&&i| i < (self.rows() + scroll - 1))
{
self.link = self.line(pos).unwrap().link;
self.link = self.lines.get(pos).unwrap().link;
}
}
LinkPos::Above => {}
@ -575,8 +487,8 @@ impl Menu {
fn action_up(&mut self) -> Action {
// no links, just scroll up
if self.link == 0 {
return if self.offset > 0 {
self.offset -= 1;
return if self.scroll > 0 {
self.scroll -= 1;
Action::Redraw
} else if !self.links.is_empty() {
self.link = self.links.len() - 1;
@ -601,8 +513,8 @@ impl Menu {
match dir {
LinkPos::Above => {
// scroll up by 1
if self.offset > 0 {
self.offset -= 1;
if self.scroll > 0 {
self.scroll -= 1;
}
// select it if it's visible now
if self.is_visible(new_link) {
@ -612,7 +524,7 @@ impl Menu {
LinkPos::Below => {
// jump to link....
if let Some(&pos) = self.links.get(new_link) {
self.offset = pos;
self.scroll = pos;
self.link = new_link;
}
}
@ -622,8 +534,8 @@ impl Menu {
self.link = new_link;
// scroll if we are within 5 lines of the top
if let Some(&pos) = self.links.get(self.link) {
if self.offset > 0 && pos < self.offset + 5 {
self.offset -= 1;
if self.scroll > 0 && pos < self.scroll + 5 {
self.scroll -= 1;
} else {
// otherwise redraw just the cursor
return self.reset_cursor(old_link);
@ -637,11 +549,11 @@ impl Menu {
}
}
/// Final `self.offset` value.
fn final_offset(&self) -> usize {
/// Final `self.scroll` value.
fn final_scroll(&self) -> usize {
let padding = (self.rows() as f64 * 0.9) as usize;
if self.spans.len() > padding {
self.spans.len() - padding
if self.lines.len() > padding {
self.lines.len() - padding
} else {
0
}
@ -664,8 +576,8 @@ impl Menu {
{
let pattern = pattern.to_ascii_lowercase();
for &pos in it {
let line = self.line(pos)?;
if line.text().to_ascii_lowercase().contains(&pattern) {
let line = self.lines.get(pos)?;
if line.text(&self.raw).to_ascii_lowercase().contains(&pattern) {
return Some(line.link);
}
}
@ -678,13 +590,13 @@ impl Menu {
// no links or final link selected already
if self.links.is_empty() || new_link >= self.links.len() {
// if there are more rows, scroll down
if self.spans.len() >= self.rows() && self.offset < self.final_offset() {
self.offset += 1;
if self.lines.len() >= self.rows() && self.scroll < self.final_scroll() {
self.scroll += 1;
return Action::Redraw;
} else if !self.links.is_empty() {
// wrap around
self.link = 0;
self.offset = 0;
self.scroll = 0;
return Action::Redraw;
}
}
@ -704,13 +616,13 @@ impl Menu {
LinkPos::Above => {
// jump to link....
if let Some(&pos) = self.links.get(new_link) {
self.offset = pos;
self.scroll = pos;
self.link = new_link;
}
}
LinkPos::Below => {
// scroll down by 1
self.offset += 1;
self.scroll += 1;
// select it if it's visible now
if self.is_visible(new_link) {
self.link = new_link;
@ -723,10 +635,10 @@ impl Menu {
self.link = new_link;
// scroll if we are within 5 lines of the end
if self.spans.len() >= self.rows() // dont scroll if content too small
&& pos >= self.offset + self.rows() - 6
if self.lines.len() >= self.rows() // dont scroll if content too small
&& pos >= self.scroll + self.rows() - 6
{
self.offset += 1;
self.scroll += 1;
} else {
// otherwise try to just re-draw the cursor
return self.reset_cursor(old_link);
@ -756,9 +668,9 @@ impl Menu {
}
} else {
if pos > 5 {
self.offset = pos - 5;
self.scroll = pos - 5;
} else {
self.offset = 0;
self.scroll = 0;
}
if !self.input.is_empty() {
Action::List(vec![self.redraw_input(), Action::Redraw])
@ -782,12 +694,12 @@ impl Menu {
if !self.is_visible(link) {
if let Some(&pos) = self.links.get(link) {
if pos > 5 {
self.offset = pos - 5;
self.scroll = pos - 5;
} else {
self.offset = 0;
self.scroll = 0;
}
if self.offset > self.final_offset() {
self.offset = self.final_offset();
if self.scroll > self.final_scroll() {
self.scroll = self.final_scroll();
}
return Action::Redraw;
}
@ -806,11 +718,11 @@ impl Menu {
self.input.clear();
if let Some(line) = self.link(self.link) {
let url = line.url();
let url = line.url(&self.raw);
let typ = gopher::type_for_url(&url);
match typ {
Type::Search => {
let prompt = format!("{}> ", line.text());
let prompt = format!("{}> ", line.text(&self.raw));
Action::Prompt(
prompt.clone(),
Box::new(move |query| {
@ -821,9 +733,9 @@ impl Menu {
}),
)
}
Type::Error => Action::Error(line.text().to_string()),
Type::Error => Action::Error(line.text(&self.raw).to_string()),
t if !t.is_supported() => Action::Error(format!("{:?} not supported", t)),
_ => Action::Open(line.text().to_string(), url),
_ => Action::Open(line.text(&self.raw).to_string(), url),
}
} else {
Action::None
@ -870,12 +782,12 @@ impl Menu {
Key::PageUp | Key::Ctrl('-') | Key::Char('-') => self.action_page_up(),
Key::PageDown | Key::Ctrl(' ') | Key::Char(' ') => self.action_page_down(),
Key::Home => {
self.offset = 0;
self.scroll = 0;
self.link = 0;
Action::Redraw
}
Key::End => {
self.offset = self.final_offset();
self.scroll = self.final_scroll();
if !self.links.is_empty() {
self.link = self.links.len() - 1;
}
@ -907,7 +819,7 @@ impl Menu {
}
}
Key::Char(c) => {
if !c.is_ascii_digit() {
if !c.is_digit(10) {
return Action::Keypress(key);
}
@ -936,8 +848,8 @@ impl Menu {
}
/// Parse gopher response into a Menu object.
pub fn parse(url: &str, raw: String, config: Config) -> Menu {
let mut spans = vec![];
pub fn parse(url: &str, raw: String) -> Menu {
let mut lines = vec![];
let mut links = vec![];
let mut longest = 0;
let mut start = 0;
@ -948,20 +860,20 @@ pub fn parse(url: &str, raw: String, config: Config) -> Menu {
break;
}
if line.is_empty() {
if line == "" {
start += 1;
continue;
}
if let Some(mut span) = parse_line(start, &raw) {
if span.text_len() > longest {
longest = span.text_len();
if let Some(mut line) = parse_line(start, &raw) {
if line.text_len() > longest {
longest = line.text_len();
}
if span.typ.is_link() {
span.link = links.len();
links.push(spans.len());
if line.typ.is_link() {
line.link = links.len();
links.push(lines.len());
}
spans.push(span);
lines.push(line);
}
start += line.len() + 1;
@ -969,26 +881,24 @@ pub fn parse(url: &str, raw: String, config: Config) -> Menu {
Menu {
url: url.into(),
spans,
lines,
links,
longest,
raw,
input: String::new(),
link: 0,
mode: Default::default(),
offset: 0,
scroll: 0,
searching: false,
size: (0, 0),
tls: false,
tor: false,
wide: false,
scroll: 0,
config,
}
}
/// Parses a single line from a Gopher menu into a `LineSpan` struct.
pub fn parse_line(start: usize, raw: &str) -> Option<LineSpan> {
/// Parses a single line from a Gopher menu into a `Line` struct.
pub fn parse_line(start: usize, raw: &str) -> Option<Line> {
if raw.is_empty() || start >= raw.len() {
return None;
}
@ -1003,48 +913,12 @@ pub fn parse_line(start: usize, raw: &str) -> Option<LineSpan> {
} else {
end
};
let typ = Type::from(line.chars().next()?).unwrap_or(Type::Binary);
let typ = Type::from(line.chars().nth(0)?)?;
let mut truncated_len = if text_end - start > MAX_COLS {
MAX_COLS + 1
} else {
text_end - start
};
let mut visible_len = truncated_len;
// if this line contains colors, calculate the visible length and
// where to truncate when abiding by `MAX_COLS`
if raw[start..text_end].contains("\x1b[") {
let mut is_color = false;
let mut iter = raw[start..text_end].char_indices();
visible_len = 0;
while let Some((i, c)) = iter.next() {
if is_color {
if c == 'm' {
is_color = false;
}
} else if c == '\x1b' {
if let Some((_, '[')) = iter.next() {
is_color = true;
}
} else if visible_len < MAX_COLS {
truncated_len = i;
visible_len += 1;
} else {
truncated_len = i;
visible_len = MAX_COLS + 1;
break;
}
}
}
Some(LineSpan {
Some(Line {
start,
end,
text_end,
truncated_len,
visible_len,
typ,
link: 0,
})
@ -1055,8 +929,8 @@ mod tests {
use super::*;
macro_rules! parse {
($s:expr) => {
parse("test", $s.to_string(), Config::default())
($s:literal) => {
parse("test", $s.to_string());
};
}
@ -1076,41 +950,29 @@ i----------- spacer localhost 70
i---------------------------------------------------------
"
);
assert_eq!(menu.spans.len(), 10);
assert_eq!(menu.lines.len(), 10);
assert_eq!(menu.links.len(), 5);
assert_eq!(
menu.lines().nth(1).unwrap().url(),
menu.lines[1].url(&menu.raw),
"gopher://gopher.club/1/phlogs/"
);
assert_eq!(menu.lines[2].url(&menu.raw), "gopher://sdf.org/1/maps/");
assert_eq!(
menu.lines().nth(2).unwrap().url(),
"gopher://sdf.org/1/maps/"
);
assert_eq!(
menu.lines().nth(3).unwrap().url(),
menu.lines[3].url(&menu.raw),
"gopher://earth.rice.edu/1Geosphere"
);
assert_eq!(menu.lines().nth(4).unwrap().text(), "wacky links");
assert_eq!(menu.lines().nth(5).unwrap().text(), "-----------");
assert_eq!(
menu.lines().nth(6).unwrap().url(),
"telnet://bbs.impakt.net:6502"
);
assert_eq!(
menu.lines().nth(7).unwrap().url(),
"https://github.com/my/code"
);
assert_eq!(menu.lines().nth(8).unwrap().text(), "-----------");
assert_eq!(menu.lines[4].text(&menu.raw), "wacky links");
assert_eq!(menu.lines[5].text(&menu.raw), "-----------");
assert_eq!(menu.lines[6].url(&menu.raw), "telnet://bbs.impakt.net:6502");
assert_eq!(menu.lines[7].url(&menu.raw), "https://github.com/my/code");
assert_eq!(menu.lines[8].text(&menu.raw), "-----------");
}
#[test]
fn test_no_path() {
let menu = parse!("1Circumlunar Space circumlunar.space 70");
assert_eq!(menu.links.len(), 1);
assert_eq!(
menu.lines().next().unwrap().url(),
"gopher://circumlunar.space"
);
assert_eq!(menu.lines[0].url(&menu.raw), "gopher://circumlunar.space");
}
#[test]
@ -1142,17 +1004,26 @@ i Err bitreich.org 70
menu.term_size(80, 40);
assert_eq!(menu.links.len(), 9);
assert_eq!(menu.link(0).unwrap().url(), "gopher://bitreich.org/1/lawn");
assert_eq!(
menu.link(1).unwrap().url(),
menu.link(0).unwrap().url(&menu.raw),
"gopher://bitreich.org/1/lawn"
);
assert_eq!(
menu.link(1).unwrap().url(&menu.raw),
"gopher://bitreich.org/1/tutorials"
);
assert_eq!(menu.link(2).unwrap().url(), "gopher://bitreich.org/1/onion");
assert_eq!(menu.link(3).unwrap().url(), "gopher://bitreich.org/1/kiosk");
assert_eq!(
menu.link(2).unwrap().url(&menu.raw),
"gopher://bitreich.org/1/onion"
);
assert_eq!(
menu.link(3).unwrap().url(&menu.raw),
"gopher://bitreich.org/1/kiosk"
);
assert_eq!(menu.link, 0);
let ssh = menu.link(4).unwrap();
assert_eq!(ssh.url(), "ssh://kiosk@bitreich.org");
assert_eq!(ssh.url(&menu.raw), "ssh://kiosk@bitreich.org");
assert_eq!(ssh.typ, Type::HTML);
menu.action_down();
@ -1171,36 +1042,9 @@ i Err bitreich.org 70
assert_eq!(menu.link, 7);
assert_eq!(menu.link(menu.link).unwrap().link, 7);
assert_eq!(menu.offset, 0);
assert_eq!(menu.scroll, 0);
menu.action_page_up();
assert_eq!(menu.link, 0);
assert_eq!(menu.link(menu.link).unwrap().link, 0);
}
#[test]
fn test_color_lines() {
let long_color_line = "ihi there. \x1b[1mthis\x1b[0m is a preeeeeety long line with \x1b[93mcolors \x1b[92mthat make it \x1b[91mseem longer than it is\x1b[0m /kiosk bitreich.org 70";
let menu = parse!(long_color_line);
let line = menu.lines().next().unwrap();
assert_eq!(long_color_line.chars().count(), 139);
assert_eq!(line.visible_len, MAX_COLS + 1);
assert_eq!(line.truncated_len, 100);
assert_eq!(
line.text_truncated(),
"hi there. \x1b[1mthis\x1b[0m is a preeeeeety long line with \x1b[93mcolors \x1b[92mthat make it \x1b[91mseem longer".to_string()
);
let long_reg_line = "1This is a regular line that is long but also has links and stuff. You are missing a gopher client? Use our kiosk mode. Thanks for coming. Hope you enjoy the fish, it's freshly grown in our lab! /kiosk bitreich.org 70";
let menu = parse!(long_reg_line);
let line = menu.lines().next().unwrap();
assert_eq!(long_color_line.chars().count(), 139);
assert_eq!(line.visible_len, MAX_COLS + 1);
assert_eq!(line.truncated_len, MAX_COLS + 1);
assert_eq!(
line.text_truncated(),
"This is a regular line that is long but also has links and stuff. You are miss"
.to_string()
);
}
}

@ -42,11 +42,11 @@ pub fn append(filename: &str, label: &str, url: &str) -> Result<()> {
path().and_then(|dotdir| {
let path = dotdir.join(filename);
if let Ok(mut file) = OpenOptions::new().append(true).create(true).open(path) {
let u = gopher::parse_url(url);
let u = gopher::parse_url(&url);
write!(
file,
"{}{}\t{}\t{}\t{}\r\n",
u.typ.to_char(),
u.typ.to_char().unwrap_or('i'),
label,
u.sel,
u.host,
@ -69,14 +69,14 @@ pub fn prepend(filename: &str, label: &str, url: &str) -> Result<()> {
.create(true)
.open(path)
{
let url = gopher::parse_url(url);
let url = gopher::parse_url(&url);
let mut buf = vec![];
file.read_to_end(&mut buf)?;
file.seek(std::io::SeekFrom::Start(0))?;
write!(
file,
"{}{}\t{}\t{}\t{}\r\n",
url.typ.to_char(),
url.typ.to_char().unwrap_or('i'),
label,
url.sel,
url.host,

@ -1,10 +1,10 @@
//! The terminal module mostly provides terminal escape sequences for
//! things like clearing the screen or going into alternate mode, as
//! well as raw mode borrowed from crossterm.
//! things like clearing the screen or going into alternate mode.
//!
//! It wraps termion for now, but we may move away from termion in the
//! future and this will help.
use lazy_static::lazy_static;
use libc::{cfmakeraw, tcgetattr, tcsetattr, termios as Termios, STDIN_FILENO, TCSANOW};
use std::{io, sync::Mutex};
use termion;
pub use termion::cursor::Goto;
pub use termion::cursor::Hide as HideCursor;
@ -14,77 +14,5 @@ pub use termion::screen::ToAlternateScreen;
pub use termion::screen::ToMainScreen;
pub use termion::clear::AfterCursor as ClearAfterCursor;
pub use termion::clear::All as ClearAll;
pub use termion::clear::CurrentLine as ClearCurrentLine;
pub use termion::clear::UntilNewline as ClearUntilNewline;
type Result<T> = std::result::Result<T, io::Error>;
lazy_static! {
// Some(Termios) -> we're in the raw mode and this is the previous mode
// None -> we're not in the raw mode
static ref TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = Mutex::new(None);
}
/// Are we in raw mode?
pub fn is_raw_mode_enabled() -> bool {
TERMINAL_MODE_PRIOR_RAW_MODE.lock().unwrap().is_some()
}
/// Go into "raw" mode, courtesy of crossterm.
pub fn enable_raw_mode() -> Result<()> {
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock().unwrap();
if original_mode.is_some() {
return Ok(());
}
let mut ios = get_terminal_attr()?;
let original_mode_ios = ios;
raw_terminal_attr(&mut ios);
set_terminal_attr(&ios)?;
// Keep it last - set the original mode only if we were able to switch to the raw mode
*original_mode = Some(original_mode_ios);
Ok(())
}
/// Back it up.
pub fn disable_raw_mode() -> Result<()> {
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock().unwrap();
if let Some(original_mode_ios) = original_mode.as_ref() {
set_terminal_attr(original_mode_ios)?;
// Keep it last - remove the original mode only if we were able to switch back
*original_mode = None;
}
Ok(())
}
// Transform the given mode into an raw mode (non-canonical) mode.
fn raw_terminal_attr(termios: &mut Termios) {
unsafe { cfmakeraw(termios) }
}
fn get_terminal_attr() -> Result<Termios> {
unsafe {
let mut termios = std::mem::zeroed();
wrap_with_result(tcgetattr(STDIN_FILENO, &mut termios))?;
Ok(termios)
}
}
fn set_terminal_attr(termios: &Termios) -> Result<bool> {
wrap_with_result(unsafe { tcsetattr(STDIN_FILENO, TCSANOW, termios) })
}
fn wrap_with_result(result: i32) -> Result<bool> {
if result == -1 {
Err(io::Error::last_os_error())
} else {
Ok(true)
}
}

@ -3,26 +3,21 @@
//! to the main UI to perform.
use crate::{
config::SharedConfig as Config,
encoding::Encoding,
config::Config,
terminal,
ui::{self, Action, Key, View, MAX_COLS},
ui::{self, Action, Key, View, MAX_COLS, SCROLL_LINES},
};
use std::{borrow::Cow, fmt, str};
use std::fmt;
/// The Text View holds the raw Gopher response as well as information
/// about which lines should currently be displayed on screen.
pub struct Text {
/// Ref to our global config
config: Config,
/// Gopher URL
url: String,
/// Gopher response
raw_response: Vec<u8>,
/// Encoded response
encoded_response: String,
raw_response: String,
/// Current scroll offset, in rows
offset: usize,
scroll: usize,
/// Number of lines
lines: usize,
/// Size of longest line
@ -35,12 +30,8 @@ pub struct Text {
pub tor: bool,
/// UI mode. Interactive (Run), Printing, Raw mode...
mode: ui::Mode,
/// Text Encoding of Response
encoding: Encoding,
/// Currently in wide mode?
pub wide: bool,
/// How many lines to scroll by. 0 = full screen
scroll: usize,
}
impl fmt::Display for Text {
@ -63,7 +54,7 @@ impl View for Text {
}
fn raw(&self) -> &str {
str::from_utf8(&self.raw_response).unwrap_or_default()
self.raw_response.as_ref()
}
fn term_size(&mut self, cols: usize, rows: usize) {
@ -78,43 +69,38 @@ impl View for Text {
self.wide
}
fn encoding(&self) -> Encoding {
self.encoding
}
fn respond(&mut self, c: Key) -> Action {
match c {
Key::Home => {
self.offset = 0;
self.scroll = 0;
Action::Redraw
}
Key::End => {
self.offset = self.final_scroll();
self.scroll = self.final_scroll();
Action::Redraw
}
Key::Ctrl('e') | Key::Char('e') => self.toggle_encoding(),
Key::Down | Key::Ctrl('n') | Key::Char('n') | Key::Ctrl('j') | Key::Char('j') => {
if self.offset < self.final_scroll() {
self.offset += 1;
if self.scroll < self.final_scroll() {
self.scroll += 1;
Action::Redraw
} else {
Action::None
}
}
Key::Up | Key::Ctrl('p') | Key::Char('p') | Key::Ctrl('k') | Key::Char('k') => {
if self.offset > 0 {
self.offset -= 1;
if self.scroll > 0 {
self.scroll -= 1;
Action::Redraw
} else {
Action::None
}
}
Key::PageUp | Key::Char('-') => {
if self.offset > 0 {
if self.offset >= self.scroll_by() {
self.offset -= self.scroll_by();
if self.scroll > 0 {
if self.scroll >= SCROLL_LINES {
self.scroll -= SCROLL_LINES;
} else {
self.offset = 0;
self.scroll = 0;
}
Action::Redraw
} else {
@ -122,9 +108,9 @@ impl View for Text {
}
}
Key::PageDown | Key::Char(' ') => {
self.offset += self.scroll_by();
if self.offset > self.final_scroll() {
self.offset = self.final_scroll();
self.scroll += SCROLL_LINES;
if self.scroll > self.final_scroll() {
self.scroll = self.final_scroll();
}
Action::Redraw
}
@ -133,19 +119,29 @@ impl View for Text {
}
fn render(&mut self) -> String {
let (_cols, rows) = self.size;
let (cols, rows) = self.size;
let mut out = String::new();
let wrap = self.config.read().unwrap().wrap;
let indent = self.indent_str(wrap);
let longest = if self.longest > MAX_COLS {
MAX_COLS
} else {
self.longest
};
let indent = if cols >= longest && cols - longest <= 6 {
String::from("")
} else if cols >= longest {
" ".repeat((cols - longest) / 2)
} else {
String::from("")
};
let limit = if self.mode == ui::Mode::Run {
rows - 1
} else {
self.lines
};
let iter = wrap_text(&self.encoded_response, wrap)
.into_iter()
.skip(self.offset)
let iter = self
.raw_response
.split_terminator('\n')
.skip(self.scroll)
.take(limit);
for line in iter {
@ -174,55 +170,30 @@ impl View for Text {
impl Text {
/// Create a Text View from a raw Gopher response and a few options.
pub fn from(url: &str, response: Vec<u8>, config: Config, tls: bool) -> Text {
let mode = config.read().unwrap().mode;
let tor = config.read().unwrap().tor;
let encoding = config.read().unwrap().encoding;
let wide = config.read().unwrap().wide;
let scroll = config.read().unwrap().scroll;
pub fn from(url: &str, response: String, config: &Config, tls: bool) -> Text {
let mut lines = 0;
let mut longest = 0;
for line in response.split_terminator('\n') {
lines += 1;
let count = line.chars().count();
if count > longest {
longest = count;
}
}
let mut new = Text {
config,
Text {
url: url.into(),
encoded_response: String::new(),
raw_response: response,
offset: 0,
lines: 0,
longest: 0,
scroll: 0,
lines,
longest,
size: (0, 0),
mode,
mode: config.mode,
tls,
tor,
encoding,
wide,
scroll,
};
new.encode_response();
new
}
/// Toggle between our two encodings.
fn toggle_encoding(&mut self) -> Action {
if matches!(self.encoding, Encoding::UTF8) {
self.encoding = Encoding::CP437;
} else {
self.encoding = Encoding::UTF8;
tor: config.tor,
wide: config.wide,
}
self.config.write().unwrap().encoding = self.encoding;
self.encode_response();
Action::Redraw
}
/// Convert the response to a Rust String and cache metadata like
/// the number of lines.
fn encode_response(&mut self) {
self.encoded_response = self.encoding.encode(&self.raw_response).into();
let wrapped = wrap_text(
self.encoded_response.as_ref(),
self.config.read().unwrap().wrap,
);
self.lines = wrapped.len();
self.longest = wrapped.iter().map(|line| line.len()).max().unwrap_or(0) as usize;
}
/// Final `self.scroll` value.
@ -234,165 +205,4 @@ impl Text {
0
}
}
/// How many lines to scroll by when paging up or down.
fn scroll_by(&self) -> usize {
if self.scroll == 0 {
self.size.1 - 1
} else {
self.scroll
}
}
/// Determine the longest line, considering any line wrapping and
/// `MAX_COL`.
fn longest_line_with_wrap(&self, wrap: usize) -> usize {
let longest = if self.longest > MAX_COLS {
MAX_COLS
} else {
self.longest
};
if wrap > 0 && longest > wrap {
wrap
} else {
longest
}
}
/// Produce the string to use for indentation, or the left margin,
/// for a given text document.
fn indent_str(&self, wrap: usize) -> Cow<str> {
let (cols, _) = self.size;
let longest = self.longest_line_with_wrap(wrap);
if cols >= longest && cols - longest <= 6 {
Cow::from("")
} else if cols >= longest {
Cow::from(" ".repeat((cols - longest) / 2))
} else {
Cow::from("")
}
}
}
/// Splits a chunk of text into a vector of strings with at most
/// `wrap` characters each. Tries to be smart and wrap at punctuation,
/// otherwise just wraps at `wrap`.
fn wrap_text(lines: &str, wrap: usize) -> Vec<&str> {
if wrap == 0 {
return lines.split('\n').collect();
}
let mut out = vec![];
for mut line in lines.lines() {
let mut len = line.chars().count();
if len > wrap {
while len > wrap {
let (end, _) = line.char_indices().take(wrap + 1).last().unwrap();
if !matches!(&line[end - 1..end], " " | "-" | "," | "." | ":") {
if let Some(&(end, _)) = line
.char_indices()
.take(wrap + 1)
.collect::<Vec<_>>()
.iter()
.rev()
.skip(1)
.find(|(_, c)| matches!(c, ' ' | '-' | ',' | '.' | ':'))
{
out.push(&line[..=end]);
if end + 1 < line.len() {
line = &line[end + 1..];
len -= end;
} else {
len = 0;
break;
}
continue;
}
}
out.push(&line[..end]);
line = &line[end..];
len -= wrap;
}
if len > 0 {
out.push(line);
}
} else {
out.push(line);
}
}
out
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_cp437() {
let body = include_bytes!("../tests/CP437.txt");
let mut text = Text::from("", body.to_vec(), Config::default(), false);
text.mode = ui::Mode::Print;
let res = text.render();
assert!(!res.contains("╟"));
assert!(!res.contains("≈"));
assert!(!res.contains("Ω"));
assert!(!res.contains("Θ"));
text.toggle_encoding();
let res = text.render();
assert!(res.contains("╟"));
assert!(res.contains("≈"));
assert!(res.contains("Ω"));
assert!(res.contains("Θ"));
}
#[test]
fn test_wrapping() {
let text = "regular line
really really really really really really really really really really long line
super duper extra scooper hoopa loopa double doopa maxi paxi giga baxi very very long line
Qua nova re oblata omnis administratio belliconsistit militesque aversi a proelio ad studium audiendi et cognoscendi feruntur ubi hostes ad legatosexercitumque pervenerunt universi se ad pedes proiciunt orant ut adventus Caesaris expectetur captamsuam urbem videre...
really really really really really really really really really kinda-but-not-really long line
another regular line
";
let lines = wrap_text(text, 70);
assert_eq!("regular line", lines[0]);
assert_eq!(
"really really really really really really really really really really",
lines[1].trim()
);
assert_eq!("long line", lines[2].trim());
assert_eq!(
"super duper extra scooper hoopa loopa double doopa maxi paxi giga",
lines[3].trim()
);
assert_eq!("baxi very very long line", lines[4].trim());
assert_eq!(
"Qua nova re oblata omnis administratio belliconsistit militesque",
lines[5].trim()
);
assert_eq!(
"aversi a proelio ad studium audiendi et cognoscendi feruntur ubi",
lines[6].trim()
);
assert_eq!(
"hostes ad legatosexercitumque pervenerunt universi se ad pedes",
lines[7].trim()
);
assert_eq!(
"really really really really really really really really really kinda-",
lines[10].trim()
);
assert_eq!("but-not-really long line", lines[11].trim());
assert_eq!(13, lines.len());
}
}

@ -1,233 +0,0 @@
//! Terminal color scheme.
//! Provides the Theme struct and functions/macros for making use of it.
use std::fmt;
/// Provides a shortcut to the Reset color code.
pub mod color {
use std::fmt;
/// Can be used with fmt calls to reset to terminal defaults.
pub struct Reset;
impl fmt::Display for Reset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "\x1b[0m")
}
}
}
/// Use with push_str() or something.
macro_rules! reset_color {
() => {
"\x1b[0m"
};
}
/// Color scheme for UI and menu items.
#[derive(Debug)]
pub struct Theme {
// UI Colors
/// The * cursor that appears next to the selected menu item.
pub ui_cursor: String,
/// The Number that appears to the left of a menu item.
pub ui_number: String,
/// The text in a menu.
pub ui_menu: String,
/// The color of the text content in a document.
pub ui_text: String,
// Menu Item Colors
/// Text document.
pub item_text: String,
/// Another menu.
pub item_menu: String,
/// Something went wrong.
pub item_error: String,
/// Gopher search prompt
pub item_search: String,
/// Telnet item. MUDs and stuff.
pub item_telnet: String,
/// External link. HTTP, usually.
pub item_external: String,
/// Binary file that can be downloaded to disk.
pub item_download: String,
/// Media that can be opened, like an image or mp3.
pub item_media: String,
/// An unknown or unsupported Gopher type.
pub item_unsupported: String,
}
impl Default for Theme {
fn default() -> Theme {
Theme {
ui_cursor: to_color("white bold"),
ui_number: to_color("magenta"),
ui_menu: to_color("yellow"),
ui_text: to_color("white"),
item_text: to_color("cyan"),
item_menu: to_color("blue"),
item_error: to_color("red"),
item_search: to_color("white"),
item_telnet: to_color("grey"),
item_external: to_color("green"),
item_download: to_color("white underline"),
item_media: to_color("green underline"),
item_unsupported: to_color("whitebg red"),
}
}
}
impl fmt::Display for Theme {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"# phetch theme
ui.cursor {ui_cursor}
ui.number {ui_number}
ui.menu {ui_menu}
ui.text {ui_text}
item.text {item_text}
item.menu {item_menu}
item.error {item_error}
item.search {item_search}
item.telnet {item_telnet}
item.external {item_external}
item.download {item_download}
item.media {item_media}
item.unsupported {item_unsupported}",
ui_cursor = to_words(&self.ui_cursor),
ui_number = to_words(&self.ui_number),
ui_menu = to_words(&self.ui_menu),
ui_text = to_words(&self.ui_text),
item_text = to_words(&self.item_text),
item_menu = to_words(&self.item_menu),
item_error = to_words(&self.item_error),
item_search = to_words(&self.item_search),
item_telnet = to_words(&self.item_telnet),
item_external = to_words(&self.item_external),
item_download = to_words(&self.item_download),
item_media = to_words(&self.item_media),
item_unsupported = to_words(&self.item_unsupported),
)
}
}
/// Convert a string like "blue underline" or "red" into a color code.
pub fn to_color<S: AsRef<str>>(line: S) -> String {
if *crate::NO_COLOR {
return "".into();
}
let parts = line.as_ref().split(' ').collect::<Vec<_>>();
if parts.is_empty() {
return "".into();
}
let mut out = String::from("\x1b[");
let len = parts.len();
for (i, part) in parts.iter().enumerate() {
out.push_str(&color_code(part).to_string());
if i < len - 1 {
out.push(';');
}
}
out.push('m');
out
}
/// Convert color code like "\x1b[91m" into something like "red"
pub fn to_words<S: AsRef<str>>(code: S) -> String {
code.as_ref()
.replace("\x1b[", "")
.replace('m', "")
.split(';')
.map(color_word)
.collect::<Vec<_>>()
.join(" ")
}
fn color_code(color: &str) -> usize {
match color {
"bold" => 1,
"underline" => 4,
"grey" => 90,
"red" => 91,
"green" => 92,
"yellow" => 93,
"blue" => 94,
"magenta" => 95,
"cyan" => 96,
"white" => 97,
"black" => 30,
"darkred" => 31,
"darkgreen" => 32,
"darkyellow" => 33,
"darkblue" => 34,
"darkmagenta" => 35,
"darkcyan" => 36,
"darkwhite" => 37,
"blackbg" => 40,
"redbg" => 41,
"greenbg" => 42,
"yellowbg" => 43,
"bluebg" => 44,
"magentabg" => 45,
"cyanbg" => 46,
"whitebg" => 47,
_ => 0,
}
}
fn color_word(code: &str) -> &'static str {
match code {
"1" => "bold",
"4" => "underline",
"90" => "grey",
"91" => "red",
"92" => "green",
"93" => "yellow",
"94" => "blue",
"95" => "magenta",
"96" => "cyan",
"97" => "white",
"30" => "black",
"31" => "darkred",
"32" => "darkgreen",
"33" => "darkyellow",
"34" => "darkblue",
"35" => "darkmagenta",
"36" => "darkcyan",
"37" => "darkwhite",
"40" => "blackbg",
"41" => "redbg",
"42" => "greenbg",
"43" => "yellowbg",
"44" => "bluebg",
"45" => "magentabg",
"46" => "cyanbg",
"47" => "whitebg",
_ => "white",
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_scheme() {
let mut theme = Theme::default();
theme.ui_cursor = to_color("bold").into();
theme.ui_menu = to_color("red").into();
theme.item_menu = to_color("blue underline").into();
assert_eq!("\u{1b}[1m", theme.ui_cursor);
assert_eq!("\u{1b}[91m", theme.ui_menu);
assert_eq!("\u{1b}[94;4m", theme.item_menu);
}
}

@ -17,33 +17,34 @@ mod view;
pub use self::{action::Action, mode::Mode, view::View};
use crate::{
bookmarks,
config::{Config, SharedConfig},
encoding::Encoding,
bookmarks, color,
config::Config,
gopher::{self, Type},
help, history,
menu::Menu,
terminal,
text::Text,
theme, utils, BUG_URL,
utils, BUG_URL,
};
use std::{
io::{stdin, stdout, Result, Write},
cell::RefCell,
io::{stdin, stdout, Result, Stdout, Write},
process::{self, Stdio},
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex, RwLock,
},
sync::mpsc,
thread,
time::Duration,
};
use termion::{input::TermRead, terminal_size};
use termion::{
input::TermRead,
raw::{IntoRawMode, RawTerminal},
terminal_size,
};
/// Alias for a termion Key event.
pub type Key = termion::event::Key;
/// Channel to receive Key events on.
pub type KeyReceiver = Arc<Mutex<Receiver<Key>>>;
/// How many lines to jump by when using page up/down.
pub const SCROLL_LINES: usize = 15;
/// How big the longest line can be, for the purposes of calculating
/// margin sizes. We often draw longer lines than this and allow
@ -54,30 +55,10 @@ pub const MAX_COLS: usize = 77;
/// (network, parsing gopher response, etc) and just show an error
/// message in the status bar, but if we can't write to STDOUT or
/// control the screen, we need to just crash.
const ERR_RAW_MODE: &str = "Fatal Error using Raw Mode.";
const ERR_SCREEN: &str = "Fatal Error using Alternate Screen.";
const ERR_STDOUT: &str = "Fatal Error writing to STDOUT.";
lazy_static! {
/// Channel to send SIGWINCH (resize) events on, once received.
static ref RESIZE_SENDER: Arc<Mutex<Option<Sender<Key>>>> = Arc::new(Mutex::new(None));
}
/// Raw resize handler that is called when SIGWINCH is received.
fn resize_handler(_: i32) {
if let Some(sender) = &*RESIZE_SENDER.lock().unwrap() {
sender.send(Key::F(5)).unwrap();
}
}
/// No-op INT handler that is called when SIGINT (ctrl-c) is
/// received in child processes (like `telnet`).
fn sigint_handler(_: i32) {}
/// Handler for when the application is resumed after ctrl-z.
fn sigcont_handler(_: i32) {
terminal::enable_raw_mode().expect("Fatal Error entering Raw Mode.");
}
/// UI is mainly concerned with drawing to the screen, managing the
/// active views, and responding to user input.
pub struct UI {
@ -94,9 +75,8 @@ pub struct UI {
/// Status message to display on screen, if any
status: String,
/// User config. Command line options + phetch.conf
config: SharedConfig,
/// Channel where UI events are sent.
keys: KeyReceiver,
config: Config,
out: RefCell<RawTerminal<Stdout>>,
}
impl UI {
@ -107,33 +87,56 @@ impl UI {
size = (cols as usize, rows as usize);
};
// Store raw terminal but don't enable it yet or switch the
// screen. We don't want to stare at a fully blank screen
// while waiting for a slow page to load.
let out = stdout().into_raw_mode().expect(ERR_RAW_MODE);
out.suspend_raw_mode().expect(ERR_RAW_MODE);
UI {
views: vec![],
focused: 0,
dirty: true,
running: true,
size,
config: Arc::new(RwLock::new(config)),
config,
status: String::new(),
keys: Self::spawn_keyboard_listener(),
out: RefCell::new(out),
}
}
/// Prepare stdout for writing. Should be used in interactive
/// mode, eg inside run()
pub fn startup(&mut self) {
let mut out = self.out.borrow_mut();
out.activate_raw_mode().expect(ERR_RAW_MODE);
write!(out, "{}", terminal::ToAlternateScreen).expect(ERR_SCREEN);
}
/// Clean up after ourselves. Should only be used after running in
/// interactive mode.
pub fn shutdown(&mut self) {
let mut out = self.out.borrow_mut();
write!(out, "{}", terminal::ToMainScreen).expect(ERR_SCREEN);
}
/// Main loop.
pub fn run(&mut self) -> Result<()> {
self.startup();
while self.running {
self.draw()?;
self.update();
}
self.shutdown();
Ok(())
}
/// Print the current view to the screen in rendered form.
pub fn draw(&mut self) -> Result<()> {
let status = self.render_status();
let mut out = stdout();
if self.dirty {
let screen = self.render()?;
let mut out = self.out.borrow_mut();
write!(
out,
"{}{}{}{}",
@ -145,6 +148,7 @@ impl UI {
out.flush()?;
self.dirty = false;
} else {
let mut out = self.out.borrow_mut();
out.write_all(status.as_ref())?;
out.flush()?;
}
@ -158,38 +162,16 @@ impl UI {
self.status.clear();
}
if let Err(e) = self.process_action(action) {
self.set_status(&format!(
"{}{}{}",
&self.config.read().unwrap().theme.item_error,
e,
terminal::HideCursor
));
}
}
/// Reload the currently focused view while preserving history.
pub fn reload(&mut self, title: &str, url: &str) -> Result<()> {
let mut rest = if self.views.len() > self.focused + 1 {
self.views.drain(self.focused..).collect()
} else {
vec![self.views.remove(self.views.len() - 1)]
};
if self.focused > 0 {
self.focused -= 1;
self.set_status(&format!("{}{}{}", color::Red, e, terminal::HideCursor));
}
self.open(title, url)?;
if rest.len() > 1 {
rest.remove(0); // drop the view we're reloading
self.views.append(&mut rest);
}
Ok(())
}
/// Open a URL - Gopher, internal, telnet, or something else.
pub fn open(&mut self, title: &str, url: &str) -> Result<()> {
// no open loops
if let Some(view) = self.views.get(self.focused) {
if view.url() == url {
return self.reload(title, url);
return Ok(());
}
}
@ -210,18 +192,6 @@ impl UI {
// binary downloads
let typ = gopher::type_for_url(url);
if typ.is_media() && self.config.read().unwrap().media.is_some() {
self.dirty = true;
return if self.config.read().unwrap().autoplay
|| self.confirm(&format!("Open in media player? {}", url))
{
utils::open_media(self.config.read().unwrap().media.as_ref().unwrap(), url)
} else {
Ok(())
};
}
if typ.is_download() {
self.dirty = true;
return if self.confirm(&format!("Download {}?", url)) {
@ -231,48 +201,21 @@ impl UI {
};
}
self.load(title, url).map(|view| {
self.load(title, url).and_then(|view| {
self.add_view(view);
})
}
/// Used to download content of the current view with a provided filename
fn download_file_with_filename(&mut self, url: &str, filename: String) -> Result<()> {
let url = url.to_string();
let (tls, tor) = (
self.config.read().unwrap().tls,
self.config.read().unwrap().tor,
);
let chan = self.keys.clone();
self.spinner(&format!("Downloading {}", url), move || {
gopher::download_url_with_filename(&url, tls, tor, chan, &filename)
})
.and_then(|res| res)
.map(|(path, bytes)| {
self.set_status(
format!(
"Download complete! {} saved to {}",
utils::human_bytes(bytes),
path
)
.as_ref(),
);
Ok(())
})
}
/// Download a binary file. Used by `open()` internally.
fn download(&mut self, url: &str) -> Result<()> {
let url = url.to_string();
let (tls, tor) = (
self.config.read().unwrap().tls,
self.config.read().unwrap().tor,
);
let chan = self.keys.clone();
let (tls, tor) = (self.config.tls, self.config.tor);
self.spinner(&format!("Downloading {}", url), move || {
gopher::download_url(&url, tls, tor, chan)
gopher::download_url(&url, tls, tor)
})
.and_then(|res| res)
.map(|(path, bytes)| {
.and_then(|(path, bytes)| {
self.set_status(
format!(
"Download complete! {} saved to {}",
@ -281,6 +224,7 @@ impl UI {
)
.as_ref(),
);
Ok(())
})
}
@ -296,25 +240,17 @@ impl UI {
thread::spawn(move || history::save(&hname, &hurl));
// request thread
let thread_url = url.to_string();
let (tls, tor) = (
self.config.read().unwrap().tls,
self.config.read().unwrap().tor,
);
let (tls, tor) = (self.config.tls, self.config.tor);
// don't spin on first ever request
let (tls, res) = if self.views.is_empty() {
gopher::fetch_url(&thread_url, tls, tor)?
} else {
self.spinner("", move || gopher::fetch_url(&thread_url, tls, tor))??
};
let typ = gopher::type_for_url(url);
let typ = gopher::type_for_url(&url);
match typ {
Type::Menu | Type::Search => Ok(Box::new(Menu::from(
url,
gopher::response_to_string(&res),
self.config.clone(),
tls,
))),
Type::Text | Type::HTML => Ok(Box::new(Text::from(url, res, self.config.clone(), tls))),
Type::Menu | Type::Search => Ok(Box::new(Menu::from(url, res, &self.config, tls))),
Type::Text | Type::HTML => Ok(Box::new(Text::from(url, res, &self.config, tls))),
_ => Err(error!("Unsupported Gopher Response: {:?}", typ)),
}
}
@ -322,15 +258,10 @@ impl UI {
/// Get Menu for on-line help, home page, etc, ex: gopher://phetch/1/help/types
fn load_internal(&mut self, url: &str) -> Result<Box<dyn View>> {
if let Some(source) = help::lookup(
url.trim_start_matches("gopher://phetch/")
&url.trim_start_matches("gopher://phetch/")
.trim_start_matches("1/"),
) {
Ok(Box::new(Menu::from(
url,
source,
self.config.clone(),
false,
)))
Ok(Box::new(Menu::from(url, source, &self.config, false)))
} else {
Err(error!("phetch URL not found: {}", url))
}
@ -360,7 +291,7 @@ impl UI {
) -> Result<T> {
let req = thread::spawn(work);
let (tx, rx) = channel();
let (tx, rx) = mpsc::channel();
let label = label.to_string();
let rows = self.rows() as u16;
thread::spawn(move || loop {
@ -375,7 +306,7 @@ impl UI {
label,
".".repeat(i),
terminal::ClearUntilNewline,
theme::color::Reset,
color::Reset,
terminal::ShowCursor,
);
stdout().flush().expect(ERR_STDOUT);
@ -420,41 +351,22 @@ impl UI {
/// Render the connection status (TLS or Tor).
fn render_conn_status(&self) -> Option<String> {
let view = self.views.get(self.focused)?;
let mut status = vec![];
if matches!(view.encoding(), Encoding::CP437) {
status.push("CP439");
}
if view.is_tls() {
if self.config.read().unwrap().emoji {
status.push("🔐");
} else {
status.push("TLS");
}
let status = color_string!("TLS", Black, GreenBG);
return Some(format!(
"{}{}",
terminal::Goto(self.cols() - 3, self.rows()),
if self.config.emoji { "🔐" } else { &status },
));
} else if view.is_tor() {
if self.config.read().unwrap().emoji {
status.push("🧅");
} else {
status.push("TOR");
}
}
if status.is_empty() {
None
} else {
let len = status.iter().fold(0, |a, s| a + s.len());
let len = len + status.len();
Some(format!(
let status = color_string!("TOR", Bold, White, MagentaBG);
return Some(format!(
"{}{}",
terminal::Goto(self.cols() - len as u16, self.rows()),
status
.iter()
.map(|s| theme::to_color("bold white") + s + reset_color!())
.collect::<Vec<_>>()
.join(" "),
))
terminal::Goto(self.cols() - 3, self.rows()),
if self.config.emoji { "🧅" } else { &status },
));
}
None
}
/// Render the status line.
@ -466,7 +378,7 @@ impl UI {
terminal::ClearCurrentLine,
self.status,
self.render_conn_status().unwrap_or_else(|| "".into()),
theme::color::Reset,
color::Reset,
)
}
@ -486,11 +398,11 @@ impl UI {
fn confirm(&self, question: &str) -> bool {
let rows = self.rows();
let mut out = stdout();
let mut out = self.out.borrow_mut();
write!(
out,
"{}{}{}{} [Y/n]: {}",
theme::color::Reset,
color::Reset,
terminal::Goto(1, rows),
terminal::ClearCurrentLine,
question,
@ -499,8 +411,12 @@ impl UI {
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
if let Ok(key) = self.keys.lock().unwrap().recv() {
matches!(key, Key::Char('\n') | Key::Char('y') | Key::Char('Y'))
if let Some(Ok(key)) = stdin().keys().next() {
match key {
Key::Char('\n') => true,
Key::Char('y') | Key::Char('Y') => true,
_ => false,
}
} else {
false
}
@ -511,11 +427,11 @@ impl UI {
let rows = self.rows();
let mut input = value.to_string();
let mut out = stdout();
let mut out = self.out.borrow_mut();
write!(
out,
"{}{}{}{}{}{}",
theme::color::Reset,
color::Reset,
terminal::Goto(1, rows),
terminal::ClearCurrentLine,
prompt,
@ -525,36 +441,39 @@ impl UI {
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
let keys = self.keys.lock().unwrap();
for key in keys.iter() {
match key {
Key::Char('\n') => {
write!(
out,
"{}{}",
terminal::ClearCurrentLine,
terminal::HideCursor
)
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
return Some(input);
}
Key::Char(c) => input.push(c),
Key::Esc | Key::Ctrl('c') => {
write!(
out,
"{}{}",
terminal::ClearCurrentLine,
terminal::HideCursor
)
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
return None;
}
Key::Backspace | Key::Delete => {
input.pop();
for k in stdin().keys() {
if let Ok(key) = k {
match key {
Key::Char('\n') => {
write!(
out,
"{}{}",
terminal::ClearCurrentLine,
terminal::HideCursor
)
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
return Some(input);
}
Key::Char(c) => input.push(c),
Key::Esc | Key::Ctrl('c') => {
write!(
out,
"{}{}",
terminal::ClearCurrentLine,
terminal::HideCursor
)
.expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
return None;
}
Key::Backspace | Key::Delete => {
input.pop();
}
_ => {}
}
_ => {}
} else {
break;
}
write!(
@ -579,8 +498,8 @@ impl UI {
/// Opens an interactive telnet session.
fn telnet(&mut self, url: &str) -> Result<()> {
let gopher::Url { host, port, .. } = gopher::parse_url(url);
terminal::disable_raw_mode()?;
let out = self.out.borrow_mut();
out.suspend_raw_mode().expect(ERR_RAW_MODE);
let mut cmd = process::Command::new("telnet")
.arg(host)
.arg(port)
@ -588,48 +507,31 @@ impl UI {
.stdout(Stdio::inherit())
.spawn()?;
cmd.wait()?;
terminal::enable_raw_mode()?;
out.activate_raw_mode().expect(ERR_RAW_MODE);
self.dirty = true; // redraw when finished with session
Ok(())
}
/// Asks the current View to process user input and produce an Action.
fn process_view_input(&mut self) -> Action {
if let Some(view) = self.views.get_mut(self.focused) {
if let Ok(key) = self.keys.lock().unwrap().recv() {
return view.respond(key);
if let Ok(key) = stdin()
.keys()
.nth(0)
.ok_or_else(|| Action::Error("stdin.keys() error".to_string()))
{
if let Ok(key) = key {
return view.respond(key);
}
}
}
Action::Error("No Gopher page loaded.".into())
}
/// Listen for keyboard events and send them along.
fn spawn_keyboard_listener() -> KeyReceiver {
let (sender, receiver) = channel();
// Give our resize handler a channel to send events on.
*RESIZE_SENDER.lock().unwrap() = Some(sender.clone());
unsafe {
libc::signal(libc::SIGWINCH, resize_handler as usize);
libc::signal(libc::SIGINT, sigint_handler as usize);
libc::signal(libc::SIGCONT, sigcont_handler as usize);
}
thread::spawn(move || {
for key in stdin().keys().flatten() {
sender.send(key).unwrap();
}
});
Arc::new(Mutex::new(receiver))
}
/// Ctrl-Z: Suspend Unix process w/ SIGTSTP.
fn suspend(&mut self) {
terminal::disable_raw_mode().expect("Fatal Error disabling Raw Mode");
let mut out = stdout();
let mut out = self.out.borrow_mut();
write!(out, "{}", terminal::ToMainScreen).expect(ERR_SCREEN);
out.flush().expect(ERR_STDOUT);
unsafe { libc::raise(libc::SIGTSTP) };
@ -655,7 +557,7 @@ impl UI {
Action::Error(e) => return Err(error!(e)),
Action::Redraw => self.dirty = true,
Action::Draw(s) => {
let mut out = stdout();
let mut out = self.out.borrow_mut();
out.write_all(s.as_ref())?;
out.flush()?;
}
@ -666,8 +568,6 @@ impl UI {
self.process_action(fun(response))?;
}
}
// F5 = redraw the display on resize
Action::Keypress(Key::F(5)) => self.dirty = true,
Action::Keypress(Key::Left) | Action::Keypress(Key::Backspace) => {
if self.focused > 0 {
self.dirty = true;
@ -683,29 +583,6 @@ impl UI {
Action::Keypress(Key::Char(key)) | Action::Keypress(Key::Ctrl(key)) => match key {
'a' => self.open("History", "gopher://phetch/1/history")?,
'b' => self.open("Bookmarks", "gopher://phetch/1/bookmarks")?,
'd' => {
let url = match self.views.get(self.focused) {
Some(view) => String::from(view.url()),
None => return Err(error!("Could not get URL from view")),
};
let url = url.as_str();
if url.starts_with("gopher://phetch/") {
return Err(error!("Can't download internal phetch pages."));
}
let u = gopher::parse_url(&url);
let default_filename = u.sel.split_terminator('/').rev().next().unwrap_or("");
if let Some(filename) = self.prompt("Save to disk as: ", default_filename) {
if filename.trim().is_empty() {
return Err(error!("Please provide a filename."));
}
match self.download_file_with_filename(url, String::from(filename)) {
Ok(()) => (),
Err(e) => return Err(error!("Download failed: {}", e)),
}
}
}
'g' => {
if let Some(url) = self.prompt("Go to URL: ", "") {
self.open(&url, &url)?;
@ -715,22 +592,16 @@ impl UI {
'r' => {
if let Some(view) = self.views.get(self.focused) {
let url = view.url();
let mut text =
Text::from(url, view.raw().into(), self.config.clone(), view.is_tls());
let raw = view.raw().to_string();
let mut text = Text::from(url, raw, &self.config, view.is_tls());
text.wide = true;
self.add_view(Box::new(text));
}
}
'R' => {
if let Some(view) = self.views.get(self.focused) {
let url = view.url().to_owned();
self.open(&url, &url)?;
}
}
's' => {
if let Some(view) = self.views.get(self.focused) {
let url = view.url();
match bookmarks::save(url, url) {
match bookmarks::save(&url, &url) {
Ok(()) => {
let msg = format!("Saved bookmark: {}", url);
self.set_status(&msg);
@ -742,22 +613,23 @@ impl UI {
'u' => {
if let Some(view) = self.views.get(self.focused) {
let current_url = view.url();
if let Some(url) = self.prompt("Current URL: ", current_url) {
self.open(&url, &url)?;
if let Some(url) = self.prompt("Current URL: ", &current_url) {
if url != current_url {
self.open(&url, &url)?;
}
}
}
}
'y' => {
if let Some(view) = self.views.get(self.focused) {
let url = view.url();
utils::copy_to_clipboard(url)?;
utils::copy_to_clipboard(&url)?;
let msg = format!("Copied {} to clipboard.", url);
self.set_status(&msg);
}
}
'w' => {
let wide = self.config.read().unwrap().wide;
self.config.write().unwrap().wide = !wide;
self.config.wide = !self.config.wide;
if let Some(view) = self.views.get_mut(self.focused) {
let w = view.wide();
view.set_wide(!w);
@ -772,3 +644,11 @@ impl UI {
Ok(())
}
}
impl Drop for UI {
fn drop(&mut self) {
let mut out = self.out.borrow_mut();
write!(out, "{}{}", color::Reset, terminal::ShowCursor).expect(ERR_STDOUT);
out.flush().expect(ERR_STDOUT);
}
}

@ -33,7 +33,11 @@ pub enum Action {
impl Action {
/// Is it Action::None?
pub fn is_none(&self) -> bool {
matches!(self, Action::None)
if let Action::None = self {
true
} else {
false
}
}
}

@ -1,6 +1,6 @@
/// The mode our text UI is in. Run mode is the default while
/// Print doesn't show the cursor, among other things.
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[derive(Debug, PartialEq, Copy, Clone)]
pub enum Mode {
/// Default, interactive mode.
/// phetch URL
@ -21,9 +21,6 @@ pub enum Mode {
/// Show command line help.
/// phetch --help
Help,
/// Print current theme
/// phetch --theme
PrintTheme,
}
impl Default for Mode {

@ -1,7 +1,5 @@
use {
crate::{encoding::Encoding, ui},
std::fmt,
};
use crate::ui;
use std::fmt;
/// Views represent what's on screen, a Gopher Menu/Text/etc item.
pub trait View: fmt::Display {
@ -25,8 +23,4 @@ pub trait View: fmt::Display {
fn wide(&mut self) -> bool;
/// Set the current screen size.
fn term_size(&mut self, cols: usize, rows: usize);
/// The current encoding.
fn encoding(&self) -> Encoding {
Encoding::default()
}
}

@ -1,6 +1,5 @@
//! Helper functions and macros.
use std::{
borrow::Cow,
io::{Result, Write},
process::{self, Stdio},
};
@ -18,8 +17,8 @@ macro_rules! log {
.open("phetch.log")
{
use std::io::prelude::*;
file.write($e.as_ref()).unwrap();
file.write(b"\n").unwrap();
file.write($e.as_ref());
file.write(b"\n");
}
}
}};
@ -36,7 +35,7 @@ macro_rules! error {
std::io::Error::new(std::io::ErrorKind::Other, $e)
};
($e:expr, $($y:expr),*) => {
error!(format!($e, $($y),*))
error!(format!($e, $($y),*));
};
}
@ -96,44 +95,3 @@ pub fn open_external(url: &str) -> Result<()> {
))
}
}
/// Opens a media file with `mpv` or `--media`.
pub fn open_media(program: &str, url: &str) -> Result<()> {
use {crate::terminal, std::io};
// mpv only supports /9/
let url = if program.ends_with("mpv") {
Cow::from(url.replace("/;/", "/9/").replace("/s/", "/9/"))
} else {
Cow::from(url)
};
// support URL: selectors
let url = if let Some(idx) = url.find("URL:") {
url.split_at(idx).1.trim_start_matches("URL:")
} else {
&url
};
let errfn = |e| {
terminal::enable_raw_mode().unwrap();
error!("Media player error: {}", e)
};
// clear screen first
let mut stdout = io::stdout();
write!(stdout, "{}{}", terminal::ClearAll, terminal::Goto(1, 1))?;
stdout.flush()?;
terminal::disable_raw_mode()?;
let mut cmd = process::Command::new(program)
.arg(url)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.spawn()
.map_err(errfn)?;
cmd.wait().map_err(errfn)?;
terminal::enable_raw_mode()?;
Ok(())
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

@ -1,19 +0,0 @@
CP437 (Extended ASCII)
<EFBFBD>ֲֲֲֲֲֲֲֲֲֲֲֲֲֲֲִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִ¿
³ € ³ <20> ³ ³ ƒ ³ „ ³ … ³ † ³ ‡ ³ ˆ ³ ‰ ³ <20> ³ ³ <20> ³ <20> ³ <20> ³ <20> ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ <20> ³ ³ ³ “ ³ ” ³ • ³ ³ — ³ ˜ ³ ™ ³ <20> ³ ³ <20> ³ <20> ³ <20> ³ <20> ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³   ³ ¡ ³ ¢ ³ £ ³ ₪ ³ ¥ ³ ¦ ³ § ³ ¨ ³ © ³ × ³ « ³ ¬ ³ ­ ³ ® ³ ¯ ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ ° ³ ± ³ ² ³ ³ ³ ´ ³ µ ³ ¶ ³ · ³ ¸ ³ ¹ ³ ÷ ³ » ³ ¼ ³ ½ ³ ¾ ³ ¿ ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ ְ ³ ֱ ³ ֲ ³ ֳ ³ ִ ³ ֵ ³ ֶ ³ ַ ³ ָ ³ ֹ ³ ֺ ³ ֻ ³ ּ ³ ֽ ³ ־ ³ ֿ ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ ׀ ³ ׁ ³ ׂ ³ ׃ ³ װ ³ ױ ³ ײ ³ ׳ ³ ״ ³ <20> ³ <20> ³ <20> ³ <20> ³ <20> ³ <20> ³ <20> ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ א ³ ב ³ ג ³ ד ³ ה ³ ו ³ ז ³ ח ³ ט ³ י ³ ך ³ כ ³ ל ³ ם ³ מ ³ ן ³
ֳִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִֵֵֵֵֵֵֵֵֵֵֵֵֵֵֵ´
³ נ ³ ס ³ ע ³ ף ³ פ ³ ץ ³ צ ³ ק ³ ר ³ ש ³ ת ³ <20> ³ <20> ³ ³ ³ <20> ³
ְֱֱֱֱֱֱֱֱֱֱֱֱֱֱֱִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִִ<EFBFBD>

@ -1,15 +0,0 @@
# phetch theme
ui.cursor red
ui.number white
ui.menu cyan
ui.text white
item.text magenta
item.menu magenta
item.error red
item.search magenta
item.telnet magenta
item.external magenta
item.download magenta underline
item.media magenta underline
item.unsupported magentabg white

@ -1,14 +0,0 @@
# phetch default theme
ui.cursor white bold
ui.number magenta
ui.menu yellow
ui.text white
item.text cyan
item.menu blue
item.error red
item.search white
item.telnet grey
item.external green
item.download white underline
item.media green underline
item.unsupported whitebg red

@ -1,15 +0,0 @@
# phetch light theme
ui.cursor black bold
ui.number magenta
ui.menu darkyellow
ui.text black
item.text darkcyan
item.menu blue
item.error red
item.search black
item.telnet grey
item.external darkgreen
item.download black underline
item.media darkgreen underline
item.unsupported redbg white

@ -1,15 +0,0 @@
# phetch matrix theme
ui.cursor yellow
ui.number grey
ui.menu green
ui.text green
item.text green
item.menu green
item.error red
item.search green
item.telnet green
item.external green
item.download green underline
item.media green underline
item.unsupported magentabg white

@ -1,15 +0,0 @@
# phetch vaporwave(ish) theme
ui.cursor red
ui.number white
ui.menu cyan
ui.text white
item.text magenta
item.menu magenta
item.error red
item.search magenta
item.telnet magenta
item.external magenta
item.download magenta underline
item.media magenta underline
item.unsupported magentabg white

@ -1,15 +0,0 @@
# phetch web theme
ui.cursor white bold
ui.number white
ui.menu white
ui.text white
item.text blue underline
item.menu blue underline
item.error red
item.search blue underline
item.telnet blue underline
item.external blue underline
item.download blue underline
item.media blue underline
item.unsupported whitebg red
Loading…
Cancel
Save