Compare commits

..

No commits in common. 'master' and 'v1.0.4' 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') }}
@ -49,17 +49,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') }}

@ -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,10 +148,6 @@ 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:
@ -205,7 +157,6 @@ jobs:
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 }}

3
.gitignore vendored

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

@ -1,150 +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
@ -167,6 +20,7 @@ the code rolling!
Enjoy!
## v1.0.2
This release fixes a few small but irritating bugs:

424
Cargo.lock generated

@ -1,436 +1,406 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
"hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "autocfg"
version = "1.1.0"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.4.3"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "c2-chacha"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
dependencies = [
"ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "cc"
version = "1.0.76"
version = "1.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a284da2e6fe2092f2353e51713435363112dfd60030e22add80be333fb928f"
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "core-foundation"
version = "0.9.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
"core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "cp437"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891a48ae269a368d5e45e0b13ee401f90d3d400da92d881f7cb67bc9da711c14"
[[package]]
name = "fastrand"
version = "1.8.0"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
dependencies = [
"instant",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
"foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "hermit-abi"
version = "0.1.19"
name = "getrandom"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "instant"
version = "0.1.12"
name = "hermit-abi"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[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.137"
version = "0.2.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "log"
version = "0.4.17"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "native-tls"
version = "0.2.11"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl 0.10.26 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)",
"schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "numtoa"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
[[package]]
name = "once_cell"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
[[package]]
name = "openssl"
version = "0.10.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12fc0523e3bd51a692c8850d075d74dc062ccf251c0110668cbd921917118a13"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.0"
version = "0.10.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c"
dependencies = [
"proc-macro2",
"quote",
"syn",
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.77"
version = "0.9.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b03b84c3b2d099b81f0953422b4d4ad58761589d0229b5506356afca05a3670a"
dependencies = [
"autocfg",
"cc",
"libc",
"pkg-config",
"vcpkg",
"autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)",
"vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "phetch"
version = "1.2.0"
version = "1.0.4"
dependencies = [
"atty",
"cp437",
"lazy_static",
"libc",
"native-tls",
"termion",
"tor-stream",
"atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"tor-stream 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "pkg-config"
version = "0.3.26"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
[[package]]
name = "proc-macro2"
version = "1.0.47"
name = "ppv-lite86"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "rand"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "quote"
version = "1.0.21"
name = "rand_chacha"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
"c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.2.16"
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags",
"rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "redox_syscall"
version = "0.1.56"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "redox_termios"
version = "0.1.2"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8440d8acb4fd3d277125b4bd01a6f38aee8d814b3b5fc09b3f2b825d37d3fe8f"
dependencies = [
"redox_syscall",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "remove_dir_all"
version = "0.5.3"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "schannel"
version = "0.1.20"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
dependencies = [
"lazy_static",
"windows-sys",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "security-framework"
version = "2.7.0"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
"core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
"core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "security-framework-sys"
version = "2.6.1"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
"core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "socks"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
dependencies = [
"byteorder",
"libc",
"winapi",
]
[[package]]
name = "syn"
version = "1.0.103"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a864042229133ada95abf3b54fdc62ef5ccabe9515b64717bcb9a1919e59445d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
"byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tempfile"
version = "3.3.0"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
"remove_dir_all",
"winapi",
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "termion"
version = "1.5.6"
version = "1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "077185e2eac69c3f8379a4298e1e07cd36beb962290d4a51199acf0fdc10607e"
dependencies = [
"libc",
"numtoa",
"redox_syscall",
"redox_termios",
"libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)",
"numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)",
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "tor-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5865109fc90e0bc0f8c299f3794ca0fd5771df988aa6b962d4c9129c39674746"
dependencies = [
"lazy_static",
"socks",
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"socks 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "winapi"
version = "0.3.9"
version = "0.2.8"
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-i686-pc-windows-gnu"
version = "0.4.0"
name = "wasi"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.36.1"
name = "winapi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
dependencies = [
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[metadata]
"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
"checksum autocfg 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2"
"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
"checksum byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c3dd8985a7111efc5c80b44e23ecdd8c007de8ade3b96595387e812b957cf5"
"checksum c2-chacha 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "214238caa1bf3a496ec3392968969cab8549f96ff30652c9e56885329315f6bb"
"checksum cc 1.0.48 (registry+https://github.com/rust-lang/crates.io-index)" = "f52a465a666ca3d838ebbf08b241383421412fe7ebb463527bba275526d89f76"
"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
"checksum core-foundation 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "25b9e03f145fd4f2bf705e07b900cd41fc636598fe5dc452fd0db1441c3f496d"
"checksum core-foundation-sys 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b"
"checksum foreign-types 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
"checksum foreign-types-shared 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
"checksum getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "e7db7ca94ed4cd01190ceee0d8a8052f08a247aa1b469a7f68c6a3b71afcf407"
"checksum hermit-abi 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eff2656d88f158ce120947499e971d743c05dbcbed62e5bd2f38f1698bbc3772"
"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
"checksum libc 0.2.66 (registry+https://github.com/rust-lang/crates.io-index)" = "d515b1f41455adea1313a4a2ac8a8a477634fbae63cc6100e3aebb207ce61558"
"checksum log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7"
"checksum native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "4b2df1a4c22fd44a62147fd8f13dd0f95c9d8ca7b2610299b2a2f9cf8964274e"
"checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef"
"checksum openssl 0.10.26 (registry+https://github.com/rust-lang/crates.io-index)" = "3a3cc5799d98e1088141b8e01ff760112bbd9f19d850c124500566ca6901a585"
"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
"checksum openssl-sys 0.9.53 (registry+https://github.com/rust-lang/crates.io-index)" = "465d16ae7fc0e313318f7de5cecf57b2fbe7511fd213978b457e1c96ff46736f"
"checksum pkg-config 0.3.17 (registry+https://github.com/rust-lang/crates.io-index)" = "05da548ad6865900e60eaba7f589cc0783590a92e940c26953ff81ddbab2d677"
"checksum ppv-lite86 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "74490b50b9fbe561ac330df47c08f3f33073d2d00c150f719147d7c54522fa1b"
"checksum rand 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3ae1b169243eaf61759b8475a998f0a385e42042370f3a7dbaf35246eacc8412"
"checksum rand_chacha 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "03a2a90da8c7523f554344f921aa97283eadf6ac484a6d2a7d0212fa7f8d6853"
"checksum rand_core 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
"checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
"checksum redox_syscall 0.1.56 (registry+https://github.com/rust-lang/crates.io-index)" = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84"
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
"checksum remove_dir_all 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4a83fa3702a688b9359eccba92d153ac33fd2e8462f9e0e3fdf155239ea7792e"
"checksum schannel 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "87f550b06b6cba9c8b8be3ee73f391990116bf527450d2556e9b9ce263b9a021"
"checksum security-framework 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8ef2429d7cefe5fd28bd1d2ed41c944547d4ff84776f5935b456da44593a16df"
"checksum security-framework-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e31493fc37615debb8c5090a7aeb4a9730bc61e77ab10b9af59f1a202284f895"
"checksum socks 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e6a64cfa9346d26e836a49fcc1ddfcb4d3df666b6787b6864db61d4918e1cbc2"
"checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
"checksum termion 1.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "c22cec9d8978d906be5ac94bceb5a010d885c626c4c8855721a4dbd20e3ac905"
"checksum tor-stream 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5865109fc90e0bc0f8c299f3794ca0fd5771df988aa6b962d4c9129c39674746"
"checksum vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "3fc439f2794e98976c88a2a2dafce96b930fe8010b0a256b3c2199a773933168"
"checksum wasi 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b89c3ce4ce14bdc6fb6beaf9ec7928ca331de5df7e5ea278375642a2f478570d"
"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
"checksum winapi 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8093091eeb260906a183e6ae1abdba2ef5ef2257a21801128899c3fc699229c6"
"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"

@ -1,6 +1,6 @@
[package]
name = "phetch"
version = "1.2.0"
version = "1.0.4"
authors = ["chris west <c@xvxx.io>"]
license = "MIT"
edition = "2018"
@ -31,13 +31,13 @@ pre-release-replacements = [
{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"
[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

@ -11,9 +11,6 @@
<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>
@ -50,21 +47,15 @@ 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,17 +70,10 @@ 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
@ -98,12 +82,12 @@ 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.4-linux-x86_64.tgz][0]
- [phetch-v1.0.4-linux-armv7.tgz (Raspberry Pi)][1]
- [phetch-v1.0.4-macos.zip][2]
Just unzip/untar the `phetch` program into your `$PATH` and get going!
@ -114,10 +98,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
@ -173,9 +153,9 @@ 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.4/phetch-v1.0.4-linux-x86_64.tgz
[1]: https://github.com/xvxx/phetch/releases/download/v1.0.4/phetch-v1.0.4-linux-armv7.tgz
[2]: https://github.com/xvxx/phetch/releases/download/v1.0.4/phetch-v1.0.4-macos.zip
[phd]: https://github.com/xvxx/phd
[aur]: https://wiki.archlinux.org/index.php/AUR_helpers
[mpv]: https://github.com/mpv-player/mpv

@ -1,11 +1,11 @@
.\" Generated by scdoc 1.11.2
.\" Generated by scdoc 1.11.0
.\" Complete documentation for this program is not available as a GNU info page
.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-09-26"
.P
.SH NAME
.P
@ -18,182 +18,151 @@ 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)
Disable Tor.
.P
.RE
\fB-m\fR, \fB--media\fR \fIPATH\fR
.RS 4
Use program at \fIPATH\fR to open media files (movies and sounds).\&
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.\&
Don't try to open media files. Download them like regular binary
Gopher items.
.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 +171,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. (Can be used to reload the current page.)
.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 +214,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 +240,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
@ -339,86 +300,6 @@ 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
@ -426,22 +307,18 @@ whitebg
.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
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.\&
\fBphetch\fR and then pressing `ctrl-h` then `3`.
.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,10 +49,6 @@ 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
@ -61,27 +57,12 @@ If no URL is given, however, *phetch* will launch and open its default
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,10 +117,8 @@ and *Ctrl-h* are synonyms.
*g*
Go to Gopher URL.
*R*
Reload current URL.
*u*
Edit URL.
Edit URL. (Can be used to reload the current page.)
*y*
Copy URL.
@ -154,8 +133,6 @@ and *Ctrl-h* are synonyms.
View raw source.
*w*
Toggle wide mode.
*e*
Toggle encoding between UTF8 and CP437.
## MENU NAVIGATION
@ -235,80 +212,6 @@ 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
@ -319,10 +222,6 @@ 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,11 +1,10 @@
# 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
- $ cargo test
- $ cargo release --dry-run --prev-tag-name=v1.x.x patch
- <edit https://github.com/xvxx/phetch/releases>
- $ cd ../[homebrew-code]
- $ make VERSION=v1.x.x

@ -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"));
}
@ -104,8 +108,6 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
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() {
@ -127,22 +129,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,17 +165,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"));
@ -206,31 +183,6 @@ pub fn parse<T: AsRef<str>>(args: &[T]) -> Result<Config, ArgError> {
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 +202,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,24 +3,13 @@
//!
//! 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";
@ -49,38 +38,8 @@ 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
@ -100,18 +59,8 @@ pub struct Config {
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 {
@ -123,12 +72,7 @@ impl Default for Config {
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 +93,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 +105,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 +117,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 +136,12 @@ 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);
@ -351,64 +230,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)
}
}
}

@ -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,56 +101,11 @@ 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.
/// Returns a tuple of:
@ -219,10 +168,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 +190,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"))?;
.next()
.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 +209,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 +281,7 @@ pub fn parse_url(url: &str) -> Url {
host = &url[..idx];
sel = &url[idx..];
} else {
host = url;
host = &url;
}
// ipv6

@ -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
@ -37,7 +37,10 @@ impl Type {
/// Text document?
pub fn is_text(self) -> bool {
matches!(self, Type::Text | Type::Xml)
match self {
Type::Text | Type::Xml => true,
_ => false,
}
}
/// HTML link?
@ -57,33 +60,36 @@ 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
)
| Type::DOSFile
| Type::UUEncoded
| Type::Binary
| Type::GIF
| Type::Image
| Type::PNG
| Type::Sound
| Type::Video
| Type::Calendar
| Type::Document => true,
_ => false,
}
}
/// Check if media to open in player
pub fn is_media(self) -> bool {
matches!(self, Type::Sound | Type::Video)
match self {
Type::Sound | Type::Video => 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 | Type::Mailbox => false,
_ => true,
}
}
/// Gopher Item Type to RFC char.

@ -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
@ -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 = "

@ -39,11 +39,10 @@ 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 +77,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,14 +1,12 @@
use phetch::{
args,
config::{Config, SharedConfig},
gopher, menu, terminal, theme,
args, color, gopher, menu, terminal,
ui::{Mode, UI},
};
use std::{
env,
error::Error,
io::{self, stdout, Write},
panic, process, str,
panic, process,
};
fn main() {
@ -28,7 +26,6 @@ fn run() -> Result<(), Box<dyn Error>> {
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 => {}
@ -80,22 +77,15 @@ Options:
-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.
-h, --help Show this screen
-v, --version Show phetch version
@ -110,7 +100,7 @@ Once you've launched phetch, use `ctrl-h` to view the on-line help."
/// 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));
println!("{}", out);
Ok(())
}
@ -120,10 +110,9 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
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());
let menu = menu::parse(url, response);
for line in menu.lines() {
out.push_str(line.text());
out.push('\n');
@ -141,12 +130,6 @@ fn print_plain(url: &str, tls: bool, tor: bool) -> Result<(), Box<dyn Error>> {
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() {
@ -167,7 +150,7 @@ fn cleanup_terminal() {
write!(
stdout,
"{}{}{}{}{}",
theme::color::Reset,
color::Reset,
terminal::ClearAll,
terminal::Goto(1, 1),
terminal::ShowCursor,

@ -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;
@ -38,8 +38,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,10 +50,6 @@ 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
@ -128,7 +124,7 @@ impl<'line, 'txt> Line<'line, 'txt> {
impl<'line, 'txt: 'line> std::ops::Deref for Line<'line, 'txt> {
type Target = LineSpan;
fn deref(&self) -> &Self::Target {
self.span
&self.span
}
}
@ -253,14 +249,13 @@ 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())
tor: config.tor,
wide: config.wide,
mode: config.mode,
..parse(url, response)
}
}
@ -292,14 +287,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 {
@ -331,9 +318,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 +334,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))
@ -365,33 +352,32 @@ impl Menu {
} else {
self.spans.len()
};
let iter = self.lines().skip(self.offset).take(limit);
let iter = self.lines().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
@ -399,25 +385,28 @@ impl Menu {
// color the line
if line.typ.is_media() {
out.push_str(&config.theme.item_media);
out.push_str(color!(Underline));
out.push_str(color!(Green));
} else if line.typ.is_download() {
out.push_str(&config.theme.item_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 +451,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,7 +475,7 @@ 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.
@ -501,8 +489,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 +498,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,7 +512,7 @@ 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) {
self.link = next_link_line.link;
@ -536,11 +524,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 +536,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()
@ -575,8 +563,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 +589,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 +600,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 +610,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,8 +625,8 @@ 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
@ -678,13 +666,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.spans.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 +692,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;
@ -724,9 +712,9 @@ impl Menu {
// 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
&& 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 +744,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 +770,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;
}
@ -870,12 +858,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 +895,7 @@ impl Menu {
}
}
Key::Char(c) => {
if !c.is_ascii_digit() {
if !c.is_digit(10) {
return Action::Keypress(key);
}
@ -936,7 +924,7 @@ impl Menu {
}
/// Parse gopher response into a Menu object.
pub fn parse(url: &str, raw: String, config: Config) -> Menu {
pub fn parse(url: &str, raw: String) -> Menu {
let mut spans = vec![];
let mut links = vec![];
let mut longest = 0;
@ -948,7 +936,7 @@ pub fn parse(url: &str, raw: String, config: Config) -> Menu {
break;
}
if line.is_empty() {
if line == "" {
start += 1;
continue;
}
@ -976,14 +964,12 @@ pub fn parse(url: &str, raw: String, config: Config) -> Menu {
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,
}
}
@ -1014,7 +1000,7 @@ pub fn parse_line(start: usize, raw: &str) -> Option<LineSpan> {
// 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[") {
if *&raw[start..text_end].contains("\x1b[") {
let mut is_color = false;
let mut iter = raw[start..text_end].char_indices();
visible_len = 0;
@ -1024,17 +1010,21 @@ pub fn parse_line(start: usize, raw: &str) -> Option<LineSpan> {
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;
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;
}
}
}
}
}
@ -1056,7 +1046,7 @@ mod tests {
macro_rules! parse {
($s:expr) => {
parse("test", $s.to_string(), Config::default())
parse("test", $s.to_string());
};
}
@ -1171,7 +1161,7 @@ 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);

@ -42,7 +42,7 @@ 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",
@ -69,7 +69,7 @@ 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))?;

@ -5,6 +5,7 @@
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;

@ -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,22 +17,23 @@ 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 lazy_static;
use libc;
use std::{
io::{stdin, stdout, Result, Write},
process::{self, Stdio},
sync::{
mpsc::{channel, Receiver, Sender},
Arc, Mutex, RwLock,
Arc, Mutex,
},
thread,
time::Duration,
@ -45,6 +46,9 @@ 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
/// wrapping in text views.
@ -73,11 +77,6 @@ fn resize_handler(_: i32) {
/// 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,7 +93,7 @@ pub struct UI {
/// Status message to display on screen, if any
status: String,
/// User config. Command line options + phetch.conf
config: SharedConfig,
config: Config,
/// Channel where UI events are sent.
keys: KeyReceiver,
}
@ -113,7 +112,7 @@ impl UI {
dirty: true,
running: true,
size,
config: Arc::new(RwLock::new(config)),
config,
status: String::new(),
keys: Self::spawn_keyboard_listener(),
}
@ -158,25 +157,18 @@ 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
));
self.set_status(&format!("{}{}{}", color::Red, 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()
self.focused -= 1;
self.views.drain(self.focused + 1..).collect()
} else {
vec![self.views.remove(self.views.len() - 1)]
vec![]
};
if self.focused > 0 {
self.focused -= 1;
}
self.open(title, url)?;
if rest.len() > 1 {
rest.remove(0); // drop the view we're reloading
@ -211,12 +203,10 @@ impl UI {
// binary downloads
let typ = gopher::type_for_url(url);
if typ.is_media() && self.config.read().unwrap().media.is_some() {
if typ.is_media() && self.config.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)
return if self.confirm(&format!("Open in media player? {}", url)) {
utils::open_media(self.config.media.as_ref().unwrap(), url)
} else {
Ok(())
};
@ -231,48 +221,22 @@ 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 (tls, tor) = (self.config.tls, self.config.tor);
let chan = self.keys.clone();
self.spinner(&format!("Downloading {}", url), move || {
gopher::download_url(&url, tls, tor, chan)
})
.and_then(|res| res)
.map(|(path, bytes)| {
.and_then(|(path, bytes)| {
self.set_status(
format!(
"Download complete! {} saved to {}",
@ -281,6 +245,7 @@ impl UI {
)
.as_ref(),
);
Ok(())
})
}
@ -296,25 +261,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 +279,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))
}
@ -375,7 +327,7 @@ impl UI {
label,
".".repeat(i),
terminal::ClearUntilNewline,
theme::color::Reset,
color::Reset,
terminal::ShowCursor,
);
stdout().flush().expect(ERR_STDOUT);
@ -420,41 +372,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 +399,7 @@ impl UI {
terminal::ClearCurrentLine,
self.status,
self.render_conn_status().unwrap_or_else(|| "".into()),
theme::color::Reset,
color::Reset,
)
}
@ -490,7 +423,7 @@ impl UI {
write!(
out,
"{}{}{}{} [Y/n]: {}",
theme::color::Reset,
color::Reset,
terminal::Goto(1, rows),
terminal::ClearCurrentLine,
question,
@ -500,7 +433,11 @@ impl UI {
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'))
match key {
Key::Char('\n') => true,
Key::Char('y') | Key::Char('Y') => true,
_ => false,
}
} else {
false
}
@ -515,7 +452,7 @@ impl UI {
write!(
out,
"{}{}{}{}{}{}",
theme::color::Reset,
color::Reset,
terminal::Goto(1, rows),
terminal::ClearCurrentLine,
prompt,
@ -614,12 +551,13 @@ impl UI {
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();
for event in stdin().keys() {
if let Ok(key) = event {
sender.send(key).unwrap();
}
}
});
@ -628,7 +566,6 @@ impl UI {
/// 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();
write!(out, "{}", terminal::ToMainScreen).expect(ERR_SCREEN);
out.flush().expect(ERR_STDOUT);
@ -666,8 +603,13 @@ impl UI {
self.process_action(fun(response))?;
}
}
// F5 = redraw the display on resize
Action::Keypress(Key::F(5)) => self.dirty = true,
// F5 = refresh
Action::Keypress(Key::F(5)) => {
if let Some(view) = self.views.get(self.focused) {
let current_url = view.url().to_owned();
self.open(&current_url, &current_url)?
}
}
Action::Keypress(Key::Left) | Action::Keypress(Key::Backspace) => {
if self.focused > 0 {
self.dirty = true;
@ -683,29 +625,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 +634,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,7 +655,7 @@ 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) {
if let Some(url) = self.prompt("Current URL: ", &current_url) {
self.open(&url, &url)?;
}
}
@ -750,14 +663,13 @@ impl UI {
'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);

@ -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()
}
}

@ -36,7 +36,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),*));
};
}

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