Author SHA1 Message Date
Stevan Freeborn 1c51f3e3a0 chore: update workspace dist config 2026-03-12 05:53:39 -05:00
Stevan Freeborn e098a0d39f chore: update workspace dist config 2026-03-12 05:51:24 -05:00
Stevan Freeborn e5c012cfe9 chore: add gitattributes file 2026-03-12 05:42:48 -05:00
Stevan Freeborn 20bc914d33 chore: remove allow-dirty 2026-03-12 05:37:12 -05:00
Stevan Freeborn b942306b29 chore: update cargo toml to allow release workflow contents to not fail workflow 2026-03-12 05:34:01 -05:00
Stevan Freeborn cfa70914a2 chore: regenerate release workflow 2026-03-12 05:28:18 -05:00
Stevan Freeborn c2b7298bb8 chore: setup github workflow for publishing using cargo-dist 2026-03-12 05:22:49 -05:00
Stevan Freeborn 4383573c8e feat: add support for opening links 2026-02-26 05:04:05 -06:00
Stevan Freeborn 65c9036ec9 fix: remove unused using 2026-02-25 17:18:34 -06:00
Stevan Freeborn b382f14a55 feat: add word wrap toggle functionality
Add the ability to toggle word wrap in the text editor with Alt+Z.
Word wrap can be enabled or disabled, affecting how long lines are
displayed in edit mode.

Changes:
- Add `is_word_wrap_on` state field with default value of false
- Implement `toggle_word_wrap()` and `is_word_wrap_on()` methods
- Add ToggleWordWrap variant to ViewAction enum
- Bind Alt+Z keyboard shortcut to toggle word wrap
- Refactor editor view to support dynamic wrapping behavior
  - Use responsive widget when wrap is enabled to constrain width
  - Apply Wrapping::WordOrGlyph when enabled, Wrapping::None otherwise
- Update editor view signature to accept word wrap state parameter
- Fix indentation inconsistencies in file.rs and state.rs
2026-02-25 17:17:34 -06:00
Stevan Freeborn 7462943c04 feat: display save indicator on new files 2026-02-21 05:31:00 -06:00
Stevan Freeborn 5faa42a3e1 feat: display indicator when need saving 2026-02-21 05:08:48 -06:00
13 changed files with 709 additions and 40 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto eol=lf
+296
View File
@@ -0,0 +1,296 @@
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.
name: Release
permissions:
"contents": "write"
# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
pull_request:
push:
tags:
- '**[0-9]+.[0-9]+.[0-9]+*'
jobs:
# Run 'dist plan' (or host) to determine what tasks we need to do
plan:
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.plan.outputs.manifest }}
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
publishing: ${{ !github.event.pull_request }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install dist
# we specify bash to get pipefail; it guards against the `curl` command
# failing. otherwise `sh` won't catch that `curl` returned non-0
shell: bash
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
- name: Cache dist
uses: actions/upload-artifact@v6
with:
name: cargo-dist-cache
path: ~/.cargo/bin/dist
# sure would be cool if github gave us proper conditionals...
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
# functionality based on whether this is a pull_request, and whether it's from a fork.
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
# but also really annoying to build CI around when it needs secrets to work right.)
- id: plan
run: |
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
echo "dist ran successfully"
cat plan-dist-manifest.json
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
name: artifacts-plan-dist-manifest
path: plan-dist-manifest.json
# Build and packages all the platform-specific things
build-local-artifacts:
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
# Let the initial task tell us to not run (currently very blunt)
needs:
- plan
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
strategy:
fail-fast: false
# Target platforms/runners are computed by dist in create-release.
# Each member of the matrix has the following arguments:
#
# - runner: the github runner
# - dist-args: cli flags to pass to dist
# - install-dist: expression to run to install dist on the runner
#
# Typically there will be:
# - 1 "global" task that builds universal installers
# - N "local" tasks that build each platform's binaries and platform-specific installers
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
steps:
- name: enable windows longpaths
run: |
git config --global core.longpaths true
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install Rust non-interactively if not already installed
if: ${{ matrix.container }}
run: |
if ! command -v cargo > /dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
fi
- name: Install dist
run: ${{ matrix.install_dist.run }}
# Get the dist-manifest
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- name: Install dependencies
run: |
${{ matrix.packages_install }}
- name: Build artifacts
run: |
# Actually do builds and make zips and whatnot
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"
- id: cargo-dist
name: Post-build
# We force bash here just because github makes it really hard to get values up
# to "real" actions without writing to env-vars, and writing to env-vars has
# inconsistent syntax between shell and powershell.
shell: bash
run: |
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Build and package all the platform-agnostic(ish) things
build-global-artifacts:
needs:
- plan
- build-local-artifacts
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
- name: Fetch local artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: cargo-dist
shell: bash
run: |
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
echo "dist ran successfully"
# Parse out what we just built and upload it to scratch storage
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
- name: "Upload artifacts"
uses: actions/upload-artifact@v6
with:
name: artifacts-build-global
path: |
${{ steps.cargo-dist.outputs.paths }}
${{ env.BUILD_MANIFEST_NAME }}
# Determines if we should publish/announce
host:
needs:
- plan
- build-local-artifacts
- build-global-artifacts
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
runs-on: "ubuntu-22.04"
outputs:
val: ${{ steps.host.outputs.manifest }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
- name: Install cached dist
uses: actions/download-artifact@v7
with:
name: cargo-dist-cache
path: ~/.cargo/bin/
- run: chmod +x ~/.cargo/bin/dist
# Fetch artifacts from scratch-storage
- name: Fetch artifacts
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: target/distrib/
merge-multiple: true
- id: host
shell: bash
run: |
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
echo "artifacts uploaded and released successfully"
cat dist-manifest.json
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
- name: "Upload dist-manifest.json"
uses: actions/upload-artifact@v6
with:
# Overwrite the previous copy
name: artifacts-dist-manifest
path: dist-manifest.json
# Create a GitHub Release while uploading all files to it
- name: "Download GitHub Artifacts"
uses: actions/download-artifact@v7
with:
pattern: artifacts-*
path: artifacts
merge-multiple: true
- name: Cleanup
run: |
# Remove the granular manifests
rm -f artifacts/*-dist-manifest.json
- name: Create GitHub Release
env:
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
RELEASE_COMMIT: "${{ github.sha }}"
run: |
# Write and read notes from a file to avoid quoting breaking things
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
announce:
needs:
- plan
- host
# use "always() && ..." to allow us to wait for all publish jobs while
# still allowing individual publish jobs to skip themselves (for prereleases).
# "host" however must run to completion, no skipping allowed!
if: ${{ always() && needs.host.result == 'success' }}
runs-on: "ubuntu-22.04"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
submodules: recursive
Generated
+276
View File
@@ -843,6 +843,17 @@ dependencies = [
"objc2 0.6.3",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dlib"
version = "0.5.2"
@@ -1142,6 +1153,15 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "form_urlencoded"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.31"
@@ -1647,6 +1667,108 @@ dependencies = [
"winit",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
dependencies = [
"displaydoc",
"potential_utf",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locale_core"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_normalizer"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
dependencies = [
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
[[package]]
name = "icu_properties"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
dependencies = [
"icu_collections",
"icu_locale_core",
"icu_properties_data",
"icu_provider",
"zerotrie",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
[[package]]
name = "icu_provider"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
dependencies = [
"displaydoc",
"icu_locale_core",
"writeable",
"yoke",
"zerofrom",
"zerotrie",
"zerovec",
]
[[package]]
name = "idna"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
dependencies = [
"idna_adapter",
"smallvec",
"utf8_iter",
]
[[package]]
name = "idna_adapter"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
dependencies = [
"icu_normalizer",
"icu_properties",
]
[[package]]
name = "image"
version = "0.25.9"
@@ -1883,6 +2005,12 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
[[package]]
name = "litemap"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "litrs"
version = "1.0.0"
@@ -2767,6 +2895,15 @@ dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.0"
@@ -3467,6 +3604,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syntect"
version = "5.3.0"
@@ -3642,6 +3790,16 @@ dependencies = [
"tracing",
]
[[package]]
name = "tinystr"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.10.0"
@@ -3801,6 +3959,24 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "url"
version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.19.0"
@@ -4076,6 +4252,22 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webbrowser"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2"
dependencies = [
"core-foundation 0.10.1",
"jni",
"log",
"ndk-context",
"objc2 0.6.3",
"objc2-foundation 0.3.2",
"url",
"web-sys",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -4676,6 +4868,12 @@ version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
[[package]]
name = "writeable"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x11-dl"
version = "2.21.0"
@@ -4760,6 +4958,29 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5"
[[package]]
name = "yoke"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
dependencies = [
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zbus"
version = "5.12.0"
@@ -4847,6 +5068,60 @@ dependencies = [
"syn",
]
[[package]]
name = "zerofrom"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [
"proc-macro2",
"quote",
"syn",
"synstructure",
]
[[package]]
name = "zerotrie"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
]
[[package]]
name = "zerovec"
version = "0.11.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "zmij"
version = "1.0.14"
@@ -4861,6 +5136,7 @@ dependencies = [
"rfd",
"tokio",
"uuid",
"webbrowser",
]
[[package]]
+6
View File
@@ -2,9 +2,15 @@
name = "zoeae"
version = "0.0.0"
edition = "2024"
repository = "https://github.com/StevanFreeborn/zoeae"
[dependencies]
iced = { version = "0.14.0", features = ["image", "markdown", "highlighter", "tokio"] }
rfd = "0.17.1"
tokio = { version ="1.49.0", features = ["fs"] }
uuid = { version = "1.19.0", features = ["v4"] }
webbrowser = "1.1.0"
[profile.dist]
inherits = "release"
lto = "thin"
+10
View File
@@ -0,0 +1,10 @@
[workspace]
members = ["cargo:."]
[dist]
cargo-dist-version = "0.31.0"
ci = "github"
installers = ["shell", "powershell"]
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
install-path = "CARGO_HOME"
install-updater = false
+63 -36
View File
@@ -2,58 +2,85 @@ use iced::{
Background, Element, Length, Theme,
border::{self},
highlighter,
widget::{container, markdown, row, scrollable, text_editor},
widget::{container, markdown, responsive, row, scrollable, text::Wrapping, text_editor},
};
use crate::{constants, file::File, message::Message, state::Mode};
pub fn view<'a>(file: &'a File, mode: Mode, font_size: u32) -> Element<'a, Message> {
pub fn view<'a>(
file: &'a File,
mode: Mode,
font_size: u32,
is_word_wrap_on: bool,
) -> Element<'a, Message> {
match mode {
Mode::Edit => {
let editor = text_editor(file.content())
.highlight(
file.extension().unwrap_or("txt"),
highlighter::Theme::Base16Ocean,
)
.padding(10)
.size(font_size)
.style(|theme: &Theme, status: text_editor::Status| {
let base = text_editor::default(theme, status);
let wrapping = if is_word_wrap_on {
Wrapping::WordOrGlyph
} else {
Wrapping::None
};
text_editor::Style {
border: border::Border {
width: 0.0,
..Default::default()
},
..base
}
})
.on_action(Message::Edit);
let create_editor = move || {
text_editor(file.content())
.wrapping(wrapping)
.highlight(
file.extension().unwrap_or("txt"),
highlighter::Theme::Base16Ocean,
)
.padding(10)
.size(font_size)
.style(|theme: &Theme, status: text_editor::Status| {
let base = text_editor::default(theme, status);
scrollable(container(editor).height(Length::Fill))
.auto_scroll(true)
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
text_editor::Style {
border: border::Border {
width: 0.0,
..Default::default()
},
..base
}
})
.on_action(Message::Edit)
};
let create_scrollable = |content: Element<'a, Message>| {
scrollable(content)
.auto_scroll(true)
.direction(scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
})
.style(|theme: &Theme, status: scrollable::Status| {
let mut style = scrollable::default(theme, status);
style.horizontal_rail.background = Some(Background::Color(theme.palette().background));
style.horizontal_rail.scroller.background = Background::Color(theme.palette().primary);
style.vertical_rail.background = Some(Background::Color(theme.palette().background));
style.vertical_rail.scroller.background = Background::Color(theme.palette().primary);
style
})
.anchor_bottom()
.height(Length::Fill)
};
if is_word_wrap_on {
responsive(move |size| {
let editor = create_editor();
create_scrollable(container(editor).width(size.width).into()).into()
})
.style(|theme: &Theme, status: scrollable::Status| {
let mut style = scrollable::default(theme, status);
style.horizontal_rail.background = Some(Background::Color(theme.palette().background));
style.horizontal_rail.scroller.background = Background::Color(theme.palette().primary);
style.vertical_rail.background = Some(Background::Color(theme.palette().background));
style.vertical_rail.scroller.background = Background::Color(theme.palette().primary);
style
})
.anchor_bottom()
.height(Length::Fill)
.into()
} else {
let editor = create_editor();
create_scrollable(container(editor).height(Length::Fill).into())
.width(Length::Fill)
.into()
}
}
Mode::Preview => {
let mut style: markdown::Style = Theme::Dark.into();
style.font = constants::CUSTOM_FONT;
let settings = markdown::Settings::with_text_size(font_size, style);
let markdown_preview = markdown::view(file.markdown(), settings).map(Message::LinkClicked);
scrollable(row![markdown_preview].padding(10))
+7 -1
View File
@@ -10,7 +10,13 @@ pub fn view<'a>(files: &'a [File], active_index: usize) -> Element<'a, Message>
let tabs = files.iter().enumerate().map(|(index, file)| {
let is_focused = index == active_index;
let label = text(file.display_name());
let label_text = if file.needs_saving() {
format!("{}", file.display_name())
} else {
file.display_name().to_owned()
};
let label = text(label_text);
let close_btn = button(text("x"))
.padding(1)
.on_press(Message::FileActionSelected(FileAction::Close(Some(index))));
+11
View File
@@ -6,6 +6,7 @@ use std::{
use iced::widget::{markdown, text_editor};
pub struct File {
needs_saving: bool,
content: text_editor::Content,
path: Option<PathBuf>,
markdown: Vec<markdown::Item>,
@@ -14,6 +15,7 @@ pub struct File {
impl Default for File {
fn default() -> Self {
File {
needs_saving: true,
content: text_editor::Content::new(),
path: None,
markdown: Vec::new(),
@@ -27,6 +29,7 @@ impl File {
let markdown = markdown::parse(content).collect();
File {
needs_saving: false,
content: text_editor_content,
path,
markdown,
@@ -90,4 +93,12 @@ impl File {
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()
}
pub fn needs_saving(&self) -> bool {
self.needs_saving
}
pub fn set_needs_saving(&mut self, state: bool) {
self.needs_saving = state
}
}
+3 -1
View File
@@ -17,7 +17,7 @@ pub fn switch_tab(state: &mut State, index: usize) -> Task<Message> {
}
pub fn link_clicked(url: String) -> Task<Message> {
println!("Opening link: {}", url);
let _ = webbrowser::open(&url);
Task::none()
}
@@ -62,6 +62,7 @@ pub fn saved_file(state: &mut State, result: Result<PathBuf, String>) -> Task<Me
match result {
Ok(path) => {
state.set_active_file_path(path);
state.set_active_file_save_status(false);
}
Err(_error) => {}
};
@@ -75,6 +76,7 @@ pub fn view_action(state: &mut State, action: ViewAction) -> Task<Message> {
ViewAction::Decrease => state.decrease_font(),
ViewAction::Reset => state.reset_font(),
ViewAction::TogglePreview => state.toggle_preview(),
ViewAction::ToggleWordWrap => state.toggle_word_wrap(),
}
Task::none()
+5
View File
@@ -65,4 +65,9 @@ pub const ALL: &[Keybinding] = &[
modifiers: Modifiers::CTRL,
message: Message::ViewActionSelected(ViewAction::Reset),
},
Keybinding {
key: "z",
modifiers: Modifiers::ALT,
message: Message::ViewActionSelected(ViewAction::ToggleWordWrap),
},
];
+6 -1
View File
@@ -84,7 +84,12 @@ fn view(state: &State, _id: iced::window::Id) -> Element<'_, Message> {
column![
components::tabs::view(state.files(), state.current_file_index()),
components::action_bar::view(state.selected_file_action(), state.selected_view_action()),
components::editor::view(current_file, state.mode(), state.font_size()),
components::editor::view(
current_file,
state.mode(),
state.font_size(),
state.is_word_wrap_on()
),
components::status_bar::view(current_file),
]
.into()
+3
View File
@@ -39,6 +39,7 @@ pub enum ViewAction {
Decrease,
Reset,
TogglePreview,
ToggleWordWrap,
}
impl ViewAction {
@@ -47,6 +48,7 @@ impl ViewAction {
ViewAction::Decrease,
ViewAction::Reset,
ViewAction::TogglePreview,
ViewAction::ToggleWordWrap,
];
}
@@ -57,6 +59,7 @@ impl Display for ViewAction {
ViewAction::Increase => write!(f, "Increase font"),
ViewAction::Reset => write!(f, "Reset font"),
ViewAction::TogglePreview => write!(f, "Toggle preview"),
ViewAction::ToggleWordWrap => write!(f, "Toggle word wrap"),
}
}
}
+22 -1
View File
@@ -22,6 +22,7 @@ pub struct State {
editor_font_size: u32,
selected_file_action: Option<FileAction>,
selected_view_action: Option<ViewAction>,
is_word_wrap_on: bool,
}
impl State {
@@ -33,12 +34,23 @@ impl State {
files: vec![default_file],
current_file: 0,
editor_font_size: constants::DEFAULT_EDITOR_FONT_SIZE,
is_word_wrap_on: false,
..Default::default()
}
}
pub fn set_active_file_save_status(&mut self, status: bool) {
self.files[self.current_file].set_needs_saving(status);
}
pub fn apply_edit(&mut self, action: text_editor::Action) {
self.files[self.current_file].content_mut().perform(action);
self.files[self.current_file]
.content_mut()
.perform(action.clone());
if matches!(action, text_editor::Action::Edit(_)) {
self.files[self.current_file].set_needs_saving(true);
}
}
pub fn new_file(&mut self) {
@@ -67,6 +79,7 @@ impl State {
pub fn open_file(&mut self, path: PathBuf, content: String) {
if let Some(index) = self.files.iter().position(|f| f.path() == Some(&path)) {
self.files[index].set_content(&content);
self.files[index].set_needs_saving(false);
self.current_file = index;
return;
}
@@ -120,6 +133,14 @@ impl State {
}
}
pub fn toggle_word_wrap(&mut self) {
self.is_word_wrap_on = !self.is_word_wrap_on;
}
pub fn is_word_wrap_on(&self) -> bool {
self.is_word_wrap_on
}
pub fn files(&self) -> &[file::File] {
&self.files
}