Highlight TUI content previews

This commit is contained in:
Georg Bauer
2026-08-04 19:04:30 +02:00
parent ccaa9204f0
commit aafde7e7e8
5 changed files with 489 additions and 20 deletions

306
Cargo.lock generated
View File

@@ -2,6 +2,12 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.4" version = "1.1.4"
@@ -157,13 +163,31 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bincode"
version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.3" version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [ dependencies = [
"bit-vec", "bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec 0.8.0",
] ]
[[package]] [[package]]
@@ -172,6 +196,12 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.3.2" version = "1.3.2"
@@ -370,6 +400,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "critical-section" name = "critical-section"
version = "1.2.0" version = "1.2.0"
@@ -491,6 +530,12 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.10.7" version = "0.10.7"
@@ -564,10 +609,21 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2"
dependencies = [ dependencies = [
"bit-set", "bit-set 0.5.3",
"regex", "regex",
] ]
[[package]]
name = "fancy-regex"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
]
[[package]] [[package]]
name = "fast-srgb8" name = "fast-srgb8"
version = "1.0.0" version = "1.0.0"
@@ -609,6 +665,16 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -689,6 +755,12 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.33" version = "0.3.33"
@@ -715,6 +787,15 @@ dependencies = [
"version_check", "version_check",
] ]
[[package]]
name = "getopts"
version = "0.2.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
dependencies = [
"unicode-width",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.17" version = "0.2.17"
@@ -818,7 +899,9 @@ dependencies = [
"crossterm", "crossterm",
"gotcha_gitea", "gotcha_gitea",
"ratatui", "ratatui",
"syntect",
"tokio", "tokio",
"tui-markdown",
] ]
[[package]] [[package]]
@@ -1121,6 +1204,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.18" version = "1.0.18"
@@ -1241,6 +1333,12 @@ dependencies = [
"bitflags 2.13.1", "bitflags 2.13.1",
] ]
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.12.1" version = "0.12.1"
@@ -1342,6 +1440,16 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.2" version = "1.2.2"
@@ -1598,6 +1706,19 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "plist"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85"
dependencies = [
"base64",
"indexmap",
"quick-xml",
"serde",
"time",
]
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.14.0" version = "1.14.0"
@@ -1619,6 +1740,25 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "pretty_assertions"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.107" version = "1.0.107"
@@ -1628,6 +1768,34 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pulldown-cmark"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
dependencies = [
"bitflags 2.13.1",
"getopts",
"memchr",
"pulldown-cmark-escape",
"unicase",
]
[[package]]
name = "pulldown-cmark-escape"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
[[package]]
name = "quick-xml"
version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "quinn" name = "quinn"
version = "0.11.11" version = "0.11.11"
@@ -1773,7 +1941,7 @@ dependencies = [
"compact_str", "compact_str",
"critical-section", "critical-section",
"hashbrown 0.17.1", "hashbrown 0.17.1",
"itertools", "itertools 0.14.0",
"kasuari", "kasuari",
"lru", "lru",
"palette", "palette",
@@ -1838,7 +2006,7 @@ dependencies = [
"hashbrown 0.17.1", "hashbrown 0.17.1",
"indoc", "indoc",
"instability", "instability",
"itertools", "itertools 0.14.0",
"line-clipping", "line-clipping",
"ratatui-core", "ratatui-core",
"serde", "serde",
@@ -1886,6 +2054,12 @@ version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.4" version = "0.13.4"
@@ -1953,6 +2127,35 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "rstest"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49"
dependencies = [
"futures-timer",
"futures-util",
"rstest_macros",
]
[[package]]
name = "rstest_macros"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn 2.0.119",
"unicode-ident",
]
[[package]] [[package]]
name = "rtoolbox" name = "rtoolbox"
version = "0.0.5" version = "0.0.5"
@@ -2291,6 +2494,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
[[package]] [[package]]
name = "simd_cesu8" name = "simd_cesu8"
version = "1.2.0" version = "1.2.0"
@@ -2439,6 +2648,27 @@ dependencies = [
"syn 2.0.119", "syn 2.0.119",
] ]
[[package]]
name = "syntect"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "656b45c05d95a5704399aeef6bd0ddec7b2b3531b7c9e900abbf7c4d2190c925"
dependencies = [
"bincode",
"fancy-regex 0.16.2",
"flate2",
"fnv",
"once_cell",
"plist",
"regex-syntax",
"serde",
"serde_derive",
"serde_json",
"thiserror 2.0.19",
"walkdir",
"yaml-rust",
]
[[package]] [[package]]
name = "tempfile" name = "tempfile"
version = "3.27.0" version = "3.27.0"
@@ -2495,7 +2725,7 @@ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"bitflags 2.13.1", "bitflags 2.13.1",
"fancy-regex", "fancy-regex 0.11.0",
"filedescriptor", "filedescriptor",
"finl_unicode", "finl_unicode",
"fixedbitset", "fixedbitset",
@@ -2590,6 +2820,7 @@ dependencies = [
"powerfmt", "powerfmt",
"serde_core", "serde_core",
"time-core", "time-core",
"time-macros",
] ]
[[package]] [[package]]
@@ -2598,6 +2829,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.3" version = "0.8.3"
@@ -2697,6 +2938,18 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "toml_edit"
version = "0.25.13+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.1.3+spec-1.1.0" version = "1.1.3+spec-1.1.0"
@@ -2764,9 +3017,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [ dependencies = [
"pin-project-lite", "pin-project-lite",
"tracing-attributes",
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]] [[package]]
name = "tracing-core" name = "tracing-core"
version = "0.1.36" version = "0.1.36"
@@ -2782,6 +3047,20 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tui-markdown"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6779056a6ae2d46013edb3692be1eb55d2e1d3f0d1d454042163efe1b64baca"
dependencies = [
"itertools 0.15.0",
"pretty_assertions",
"pulldown-cmark",
"ratatui-core",
"rstest",
"tracing",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.20.1" version = "1.20.1"
@@ -2818,7 +3097,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
dependencies = [ dependencies = [
"itertools", "itertools 0.14.0",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
] ]
@@ -3373,6 +3652,21 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "yaml-rust"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
dependencies = [
"linked-hash-map",
]
[[package]]
name = "yansi"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.3" version = "0.8.3"

View File

@@ -47,9 +47,10 @@ representative repositories.
- [ ] The five numbered panes open Home, Issues, Repos, PRs, and Milestones; - [ ] The five numbered panes open Home, Issues, Repos, PRs, and Milestones;
the selected pane and row remain visibly distinct at narrow and wide the selected pane and row remain visibly distinct at narrow and wide
terminal sizes. terminal sizes.
- [ ] `j`/`k`, arrows, `g`/`G`, Page Up/Down, Enter, Backspace, and `q` provide - [ ] `j`/`k`, arrows, `g`/`G`, Page Up/Down, Enter, and Backspace provide
mutt-style list and history navigation; `n`/`p` and scrolling past a mutt-style list and history navigation; `q` exits immediately from every
boundary traverse paginated API results without losing history. non-editor depth. `n`/`p` and scrolling past a boundary traverse
paginated API results without losing history.
- [ ] Clicking a pane switches it, clicking a list row selects it, - [ ] Clicking a pane switches it, clicking a list row selects it,
double-clicking opens it, and the mouse wheel scrolls the list. double-clicking opens it, and the mouse wheel scrolls the list.
- [ ] Home activity filters cycle with `v`; activity targets route to their - [ ] Home activity filters cycle with `v`; activity targets route to their
@@ -57,6 +58,12 @@ representative repositories.
- [ ] Repository favorites toggle with `*`, sort first per pane, and persist - [ ] Repository favorites toggle with `*`, sort first per pane, and persist
after relaunch. Commits switch branches with `b`; file trees, text and after relaunch. Commits switch branches with `b`; file trees, text and
binary files, commit changes, pull changes, and per-file diffs open. binary files, commit changes, pull changes, and per-file diffs open.
- [ ] Source files use grammar-based syntax colors selected by filename or
shebang, with unknown text remaining legible. Commit and pull diffs show
metadata, hunks, additions, and deletions in distinct syntax colors.
- [ ] Home activity content plus issue, pull request, milestone, and comment
Markdown pseudo-renders headings, emphasis, strong text, lists, links,
inline code, and fenced code while preserving metadata line breaks.
- [ ] Issue and pull filters apply. Issues can be created, edited, - [ ] Issue and pull filters apply. Issues can be created, edited,
closed/reopened, and deleted after confirmation; issue comments can be closed/reopened, and deleted after confirmation; issue comments can be
added and the current user's comments edited. added and the current user's comments edited.

View File

@@ -14,4 +14,6 @@ path = "src/main.rs"
crossterm = "0.29" crossterm = "0.29"
gotcha_gitea = { path = "../gitea" } gotcha_gitea = { path = "../gitea" }
ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "layout-cache", "macros", "underline-color"] } ratatui = { version = "0.30", default-features = false, features = ["crossterm_0_29", "layout-cache", "macros", "underline-color"] }
syntect = { version = "5.3", default-features = false, features = ["default-fancy"] }
tokio.workspace = true tokio.workspace = true
tui-markdown = { version = "0.3", default-features = false }

View File

@@ -1324,7 +1324,7 @@ impl App {
), ),
); );
screen.detail = format!( screen.detail = format!(
"State: {state}\nAuthor: {}\nUpdated: {}\n\n{}\n\n[e] edit [c] comment [x] state [d] delete", "State: {state} \nAuthor: {} \nUpdated: {}\n\n{}\n\n[e] edit [c] comment [x] state [d] delete",
issue issue
.user .user
.as_ref() .as_ref()
@@ -1545,7 +1545,7 @@ impl App {
screen.page = page; screen.page = page;
screen.has_more = details.has_more; screen.has_more = details.has_more;
screen.detail = format!( screen.detail = format!(
"State: {}\n{}{}\nFiles: {} +{} -{}\n\n{}", "State: {} \n{}{} \nFiles: {} +{} -{}\n\n{}",
gotcha_gitea::pull_state(&pull), gotcha_gitea::pull_state(&pull),
pull.head pull.head
.as_ref() .as_ref()
@@ -1642,7 +1642,7 @@ impl App {
screen.page = page; screen.page = page;
screen.has_more = details.has_more; screen.has_more = details.has_more;
screen.detail = format!( screen.detail = format!(
"State: {}\nDue: {}\nOpen: {} Closed: {}\n\n{}\n\n[e] edit [x] state [d] delete", "State: {} \nDue: {} \nOpen: {} Closed: {}\n\n{}\n\n[e] edit [x] state [d] delete",
milestone.state.as_deref().unwrap_or("unknown"), milestone.state.as_deref().unwrap_or("unknown"),
milestone.due_on.as_deref().unwrap_or("none"), milestone.due_on.as_deref().unwrap_or("none"),
milestone.open_issues.unwrap_or_default(), milestone.open_issues.unwrap_or_default(),

View File

@@ -1,3 +1,5 @@
use std::{cell::RefCell, sync::LazyLock};
use ratatui::{ use ratatui::{
Frame, Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect}, layout::{Alignment, Constraint, Direction, Layout, Rect},
@@ -5,13 +7,24 @@ use ratatui::{
text::{Line, Span, Text}, text::{Line, Span, Text},
widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, Wrap}, widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, Wrap},
}; };
use syntect::{
easy::HighlightLines,
highlighting::{FontStyle, ThemeSet},
parsing::SyntaxSet,
util::LinesWithEndings,
};
use crate::{ use crate::{
app::{App, Tab}, app::{App, ScreenKind, Tab},
editor::Editor, editor::Editor,
}; };
const ACCENT: Color = Color::Cyan; const ACCENT: Color = Color::Cyan;
static SYNTAXES: LazyLock<SyntaxSet> = LazyLock::new(SyntaxSet::load_defaults_newlines);
static THEMES: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);
thread_local! {
static HIGHLIGHT_CACHE: RefCell<Option<(String, String, Text<'static>)>> = const { RefCell::new(None) };
}
pub fn draw(frame: &mut Frame<'_>, app: &mut App) { pub fn draw(frame: &mut Frame<'_>, app: &mut App) {
let area = frame.area(); let area = frame.area();
@@ -167,25 +180,127 @@ fn lane_color(lane: usize) -> Color {
} }
fn draw_detail(frame: &mut Frame<'_>, app: &App, area: Rect) { fn draw_detail(frame: &mut Frame<'_>, app: &App, area: Rect) {
let mut detail = app.screen.detail.clone(); let mut detail = render_screen_detail(&app.screen.kind, &app.screen.detail);
if let Some(item) = app.screen.selected_item() if let Some(item) = app.screen.selected_item()
&& !item.detail.is_empty() && !item.detail.is_empty()
{ {
if !detail.is_empty() { if !detail.lines.is_empty() {
detail.push_str("\n\n────\n\n"); detail.lines.extend([
Line::default(),
Line::styled("────", Style::default().fg(Color::DarkGray)),
Line::default(),
]);
} }
detail.push_str(&item.detail); detail
.lines
.extend(render_item_detail(&app.screen.kind, &item.detail).lines);
} }
if detail.is_empty() { if detail.lines.is_empty() {
detail = "Select an item to see details.".into(); detail = Text::from("Select an item to see details.");
} }
let paragraph = Paragraph::new(Text::from(detail)) let paragraph = Paragraph::new(detail)
.block(Block::default().borders(Borders::ALL).title(" Preview ")) .block(Block::default().borders(Borders::ALL).title(" Preview "))
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
.scroll((app.screen.detail_scroll, 0)); .scroll((app.screen.detail_scroll, 0));
frame.render_widget(paragraph, area); frame.render_widget(paragraph, area);
} }
fn render_screen_detail<'a>(kind: &ScreenKind, detail: &'a str) -> Text<'a> {
match kind {
ScreenKind::File(_, path) => highlight(detail, path),
ScreenKind::Text(_, _) if detail.starts_with("diff --git ") => {
highlight(detail, "change.diff")
}
ScreenKind::Text(title, _) => highlight(detail, title),
ScreenKind::Issue(_, _) | ScreenKind::Pull(_, _) | ScreenKind::Milestone(_, _) => {
render_markdown(detail)
}
_ => Text::from(detail),
}
}
fn render_item_detail<'a>(kind: &ScreenKind, detail: &'a str) -> Text<'a> {
if detail.starts_with("diff --git ") {
highlight(detail, "change.diff")
} else if matches!(
kind,
ScreenKind::Home
| ScreenKind::Issue(_, _)
| ScreenKind::Pull(_, _)
| ScreenKind::Milestone(_, _)
) {
render_markdown(detail)
} else {
Text::from(detail)
}
}
fn render_markdown(markdown: &str) -> Text<'_> {
tui_markdown::from_str(markdown)
}
fn highlight(source: &str, path: &str) -> Text<'static> {
HIGHLIGHT_CACHE.with_borrow_mut(|cache| {
if let Some((cached_path, cached_source, text)) = cache.as_ref()
&& cached_path == path
&& cached_source == source
{
return text.clone();
}
let text = highlight_uncached(source, path);
*cache = Some((path.to_owned(), source.to_owned(), text.clone()));
text
})
}
fn highlight_uncached(source: &str, path: &str) -> Text<'static> {
let syntax = SYNTAXES
.find_syntax_for_file(path)
.ok()
.flatten()
.or_else(|| SYNTAXES.find_syntax_by_first_line(source))
.unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
let theme = &THEMES.themes["base16-ocean.dark"];
let mut highlighter = HighlightLines::new(syntax, theme);
let lines = LinesWithEndings::from(source)
.map(|line| match highlighter.highlight_line(line, &SYNTAXES) {
Ok(ranges) => {
let last = ranges.len().saturating_sub(1);
Line::from(
ranges
.into_iter()
.enumerate()
.map(|(index, (style, text))| {
let text = if index == last {
text.trim_end_matches(['\r', '\n'])
} else {
text
};
let mut terminal_style = Style::default().fg(Color::Rgb(
style.foreground.r,
style.foreground.g,
style.foreground.b,
));
if style.font_style.contains(FontStyle::BOLD) {
terminal_style = terminal_style.add_modifier(Modifier::BOLD);
}
if style.font_style.contains(FontStyle::ITALIC) {
terminal_style = terminal_style.add_modifier(Modifier::ITALIC);
}
if style.font_style.contains(FontStyle::UNDERLINE) {
terminal_style = terminal_style.add_modifier(Modifier::UNDERLINED);
}
Span::styled(text.to_owned(), terminal_style)
})
.collect::<Vec<_>>(),
)
}
Err(_) => Line::from(line.trim_end_matches(['\r', '\n']).to_owned()),
})
.collect::<Vec<_>>();
Text::from(lines)
}
fn draw_footer(frame: &mut Frame<'_>, app: &App, area: Rect) { fn draw_footer(frame: &mut Frame<'_>, app: &App, area: Rect) {
let status = if let Some(status) = app.visible_status() { let status = if let Some(status) = app.visible_status() {
format!( format!(
@@ -357,4 +472,55 @@ mod tests {
assert_eq!(buffer[(4, 0)].fg, lane_color(1)); assert_eq!(buffer[(4, 0)].fg, lane_color(1));
assert_eq!(buffer[(6, 0)].fg, lane_color(2)); assert_eq!(buffer[(6, 0)].fg, lane_color(2));
} }
#[test]
fn previews_render_markdown_source_and_diffs_with_styles() {
let repository = gotcha_gitea::RepositoryId {
owner: "owner".into(),
repository: "project".into(),
};
let markdown = render_item_detail(
&ScreenKind::Home,
"# Title\n\nSome *emphasis* and **strong text**.",
);
assert!(
markdown.lines[0]
.style
.add_modifier
.contains(Modifier::BOLD)
);
assert!(
markdown
.lines
.iter()
.flat_map(|line| &line.spans)
.any(|span| { span.style.add_modifier.contains(Modifier::ITALIC) })
);
let source = render_screen_detail(
&ScreenKind::File(repository, "main.rs".into()),
"fn main() { println!(\"hello\"); }\n",
);
let source_colors = source
.lines
.iter()
.flat_map(|line| &line.spans)
.filter_map(|span| span.style.fg)
.collect::<Vec<_>>();
assert!(
source_colors
.iter()
.any(|color| Some(color) != source_colors.first())
);
let diff_source = "diff --git a/main.rs b/main.rs\n@@ -1 +1 @@\n-old\n+new\n";
let diff = render_screen_detail(
&ScreenKind::Text("main.rs".into(), diff_source.into()),
diff_source,
);
assert_ne!(
diff.lines[2].spans[0].style.fg,
diff.lines[3].spans[0].style.fg
);
}
} }