Showing preview only (220K chars total). Download the full file or copy to clipboard to get everything.
Repository: gin66/tui-logger
Branch: master
Commit: a3d46e0eccc4
Files: 42
Total size: 207.5 KB
Directory structure:
gitextract_gkuhw83x/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── build_examples.yml
│ ├── build_examples_latest.yml
│ ├── cargo_test.yml
│ ├── docs.yml
│ └── semver_checks.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── DEV_NOTES.md
├── LICENSE
├── README.md
├── bacon.toml
├── doc/
│ ├── demo-short.tape
│ └── demo.tape
├── examples/
│ ├── demo.rs
│ └── slog.rs_outdated
├── src/
│ ├── circular.rs
│ ├── config/
│ │ ├── level_config.rs
│ │ └── mod.rs
│ ├── file.rs
│ ├── lib.rs
│ ├── logger/
│ │ ├── api.rs
│ │ ├── fast_hash.rs
│ │ ├── inner.rs
│ │ └── mod.rs
│ ├── slog.rs
│ ├── tracing_subscriber.rs
│ └── widget/
│ ├── inner.rs
│ ├── logformatter.rs
│ ├── mod.rs
│ ├── smart.rs
│ ├── standard.rs
│ ├── standard_formatter.rs
│ └── target.rs
└── tests/
├── empty_log.rs
├── envfilter.rs
├── formatter_wrap.rs
├── scroll.rs
├── scroll_long_wrap.rs
├── scroll_wrap.rs
└── simple.rs
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms
github: [gin66] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
================================================
FILE: .github/workflows/build_examples.yml
================================================
name: Build examples
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check rust version
run: rustup show
- name: Build examples with termion
run: cargo build --examples --features termion
- name: Build example with crossterm
run: cargo build --examples --features crossterm
- name: Build examples with termion and custom formatter
run: cargo build --examples --features termion,formatter
================================================
FILE: .github/workflows/build_examples_latest.yml
================================================
name: Build examples with latest rust version
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check rust version
run: rustup show
- name: Update rust
run: rustup update
- name: Build examples with ratatui and termion
run: cargo build --examples --features termion
- name: Build example with ratatui and crossterm
run: cargo build --examples --features crossterm
================================================
FILE: .github/workflows/cargo_test.yml
================================================
name: cargo test
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check rust version
run: rustup show
- name: Run tests
run: |
cargo test
cargo test -F tracing-support --doc
================================================
FILE: .github/workflows/docs.yml
================================================
name: Documentation
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
check-docs:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install cargo-rdme
uses: taiki-e/install-action@v2
with:
tool: cargo-rdme
- name: Check README.md is up-to-date
run: cargo rdme --check
================================================
FILE: .github/workflows/semver_checks.yml
================================================
name: Semver Checks
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
semver-checks:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check semver
uses: obi1kenobi/cargo-semver-checks-action@v2
================================================
FILE: .gitignore
================================================
/target
**/*.rs.bk
Cargo.lock
.DS_Store
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.18.2](https://github.com/gin66/tui-logger/compare/v0.18.1...v0.18.2) - 2026-04-01
### Other
- switch from chrono to jiff ([#96](https://github.com/gin66/tui-logger/pull/96))
## [0.18.1](https://github.com/gin66/tui-logger/compare/v0.18.0...v0.18.1) - 2026-01-19
### Fixed
- *(formatter)* Prevent panic on empty log message
## [0.18.0](https://github.com/gin66/tui-logger/compare/v0.17.4...v0.18.0) - 2025-12-28
### Other
- update to ratatui v0.30 as proposed by [#93](https://github.com/gin66/tui-logger/pull/93)
## [0.17.4](https://github.com/gin66/tui-logger/compare/v0.17.3...v0.17.4) - 2025-09-28
### Fixed
- fix lifetime warning in demo.rs
- fix cargo clippy warnings by opencode+grok
- fix documentation
- fix README, which is controlled by lib.rs
### Other
- run cargo fmt
- resolve lifetime warnings as proposed by compiler
- resolve security warning for fxhash by eliminating it ([#91](https://github.com/gin66/tui-logger/pull/91))
- Create FUNDING.yml
## [0.17.3](https://github.com/gin66/tui-logger/compare/v0.17.2...v0.17.3) - 2025-05-24
### Other
- update Readme
- add ai generated documentation reference
- add attributes of the current span for tracing subscriber (#90)
- work on Readme
## [0.17.2](https://github.com/gin66/tui-logger/compare/v0.17.1...v0.17.2) - 2025-04-23
### Other
- update README
- Fix invalid docs for TuiTracingSubscriverLayer (#87)
## [0.17.1](https://github.com/gin66/tui-logger/compare/v0.17.0...v0.17.1) - 2025-04-13
### Other
- run cargo fmt
- use env_filter for configuration by environment variable as proposed by [#86](https://github.com/gin66/tui-logger/pull/86)
- remove stray comment line in tests
## [0.17.0](https://github.com/gin66/tui-logger/compare/v0.16.0...v0.17.0) - 2025-03-11
### Other
- file/module_path/line are now Option, because log::Record defines them as such, too
## [0.16.0](https://github.com/gin66/tui-logger/compare/v0.15.0...v0.16.0) - 2025-03-10
### Other
- ExtLogRecord stores for file and module_path their static strings, if available. In addition provides module_path getter
- provide accessors for the strings
- update DEV_NOTES
## [0.15.0](https://github.com/gin66/tui-logger/compare/v0.14.5...v0.15.0) - 2025-03-08
### Fixed
- fix bug with page up at top
### Other
- lib.rs factoring complete
- refactor TuiLoggerError out of lib.rs
- refactor TuiLogger API out of lib.rs
- refactor TuiLogger out of lib.rs
- refactor LevelConfig out of lib.rs
- refactor TuiLoggerTargetWidget out of lib.rs
- reduce visibility of internal structures
- add test case for log message being larger than widget
- turn off debugging
- add test case with standard formatter
- add test with wrapped lines
- disable println in lib code with render_debug switch
- intermediate version with debug prints
- circular buffer allows absolute indexing
- test cases for standard widget display and scroll
## [0.14.5](https://github.com/gin66/tui-logger/compare/v0.14.4...v0.14.5) - 2025-02-22
### Other
- update Changelog and Readme
- Made target width also use unicode_segmentation
- Use unicode-segmentation for splitting chars
- Split lines safely
- change &mut to & reference in TuiWidgetState.transition as [#83](https://github.com/gin66/tui-logger/pull/83)
- Update mermaid diagram in README.md
- Update lib.rs for readme change
- Update README.md
- Update README.md with mermaid diagram
- rename dev_notes.md
- cleanup files in doc
- udpate readme
- new demo of the widget using vhs
- rename cargo test workflow
- remove stray space in github action
## [0.14.4](https://github.com/gin66/tui-logger/compare/v0.14.3...v0.14.4) - 2025-01-31
### Fixed
- fix cargo test
### Other
- update Readme for custom formatter example in demo
- example demo extended to use custom formatter in one of the widgets
- run cargo test in github actions
## [0.14.3](https://github.com/gin66/tui-logger/compare/v0.14.2...v0.14.3) - 2025-01-31
### Other
- work on Readme and add formatter() to smartwidget
- standard formatter appears to work as before, but using Line/Span
- assure LogFormatter Send+Sync
- implement formatter trait as discussed in [#77](https://github.com/gin66/tui-logger/pull/77) and [#82](https://github.com/gin66/tui-logger/pull/82)
## [0.14.2](https://github.com/gin66/tui-logger/compare/v0.14.1...v0.14.2) - 2025-01-30
### Fixed
- fix warnings
### Other
- split lib.rs into several files
- Merge pull request [#77](https://github.com/gin66/tui-logger/pull/77) from tofubert/add_style_for_file
- Merge pull request [#78](https://github.com/gin66/tui-logger/pull/78) from andrei-ng/fix-order-of-fields-tracing-feature
- Merge pull request [#79](https://github.com/gin66/tui-logger/pull/79) from andrei-ng/skip-printing-message-key
- use env::temp_dir for demo log file target
- do not print the 'message' key in the formatter for tracing support
- fix formatter for tracing events
- make comment for file logging a bit better
- give file logging format options
- Update CHANGELOG.md
0.14.1:
- re-export log::LevelFilter
0.14.0:
- Update version of ratatui
0.13.2:
- fix tracing support
0.13.1:
- fix missing `move_events()` on half full buffer in case hot buffer capacity was odd
0.13.0:
- `move_events()` is not published anymore, but called by a cyclic internal task.
This task is called by timeout (10ms) unless the hot buffer is half full.
- `init_logger()` returns now `Result<(), TuiLoggerError>`
0.12.1:
- fix for issue #69: avoid unwrap panic by using default level
- add `set_buffer_depth()` to modify circular buffer size
0.12.0:
- update ratatui to 0.28
0.11.2:
- update ratatui to 0.27
0.11.1:
- one line error report for demo example, if feature flag crossterm or termion not given
- use cargo readme and test in github build
- Fix of issue #60: panic on too small widget size
0.11.0:
- BREAKING CHANGE: TuiWidgetEvent::transition() method now takes a TuiWidgetEvent by value instead of by reference.
- update ratatui to 0.25
0.10.1:
- update ratatui to ^0.25.0
0.10.0:
- Remove support for tui legacy crate
- Use `Cell::set_symbol()` as performance optimization from ratatui
- Require chrono >= 0.4.20 for avoid security vulnerability (https://rustsec.org/advisories/RUSTSEC-2020-0159.html)
0.9.6:
- update ratatui to 0.23.0
0.9.5:
- rework examples/demo to not use termion
0.9.4:
- fix breaking change in 0.9.3 as reported by issue #43
0.9.3:
- update to ratatui 0.22 and fix clippy warnings
0.9.2:
- update to ratatui 0.21
0.9.1:
- Implement Eq for TuiWidgetEvent
- suppport `border_type()` for TuiLoggerSmartWidget
- Disable default features of chrono to avoid import of `time` v0.1.x
0.9.0:
- support for tracing-subscriber
- add optional ratatui support as proposed by (#32)
- slog is NOT a default feature anymore. Enable with `slog-support`
0.8.3:
- Make `TuiWidgetState.set_default_display_level()` work for TuiLoggerWidget (#30)
0.8.2:
- Allow TuiLoggerWidget to be controlled with TuiWidgetState by calling state() builder function (#30)
- Extend demo for an example for this TuiLoggerWidget control
0.8.1:
- Update to tui-rs 0.19 and slog to 2.7.0
0.8.0:
- Update to tui-rs 0.18
0.7.1:
- Update to tui-rs 0.17
0.7.0:
- Update rust edition in Cargo.toml to 2021
- Fix all warnings from cargo clippy
- new function for TuiWidgetState to set the default display level - not impacting the recording
```rust
set_default_display_level(self, levelfilter: LevelFilter) -> TuiWidgetState
- changed signature for TuiWidgetState function from
```rust
set_level_for_target(&self, target: &str, levelfilter: LevelFilter) -> &TuiWidgetState
```
to
```rust
set_level_for_target(self, target: &str, levelfilter: LevelFilter) -> TuiWidgetState
```
0.6.6:
- Add functions to format output of log data as discussed in [issue #19](https://github.com/gin66/tui-logger/issues/19)
The functions are with their default values:
```
output_separator(':')
output_timestamp(Some("%H:%M:%S".to_string()))
output_level(Some(TuiLoggerLevelOutput::Long))
output_target(true)
output_file(true)
output_line(true)
```
0.6.5:
- Use thread safe counterparts for Rc/RefCell
0.6.4:
- Bump version up for update to tui 0.16
0.6.3:
- Removed verbose timestamp info log (issue #16)
0.6.2:
- Fix by Wuelle to avoid panic on line wrapping inside a utf8 character
0.6.1:
- Changes in README
0.6.0:
- Support Scrollback in log history with TuiWidgetEvent::PrevPageKey, NextPageKey and EscapeKey
- log and target panes' title can be set via .title_log(String) and .title_target(String)
0.5.1:
- TuiWidgetEvent is now Debug, Clone, PartialEq, Hash
0.5.0:
- Remove dispatcher completely
- Get rid of dependency to termion and crossterm
- KeyCommands to be translated by the application into KeyEvents for TuiWidgetState::transition()
================================================
FILE: Cargo.toml
================================================
[package]
name = "tui-logger"
version = "0.18.2"
authors = ["Jochen Kiemes <jochen@kiemes.de>"]
edition = "2021"
license = "MIT"
description = "Logger with smart widget for the `ratatui` crate"
documentation = "https://docs.rs/tui-logger/latest/tui_logger/"
repository = "https://github.com/gin66/tui-logger"
readme = "README.md"
keywords = ["tui", "log", "logger", "widget", "dispatcher"]
[dependencies]
log = "0.4"
jiff = "0.2"
ratatui = { version = "0.30", default-features = false}
tracing = {version = "0.1.40", optional = true}
tracing-subscriber = {version = "0.3", optional = true}
lazy_static = "1.5"
parking_lot = "0.12"
slog = { version = "2.7.0", optional = true }
unicode-segmentation = "1.12.0"
env_filter = "0.1.3"
[dev-dependencies]
# the crate is compatible with ratatui >=0.25.0, but the demo uses features from 0.27.0
ratatui = { version = "0.30", default-features = false}
anyhow = "1.0.91"
env_logger = "0.11.5"
termion = {version = "4.0.3" }
crossterm = {version = "0.29"}
[features]
slog-support = ["slog"]
tracing-support = ["tracing", "tracing-subscriber"]
# only necessary for the demo, the crate does has no dependencies on these
#
# feature_crossterm_or_termion_must_be_selected to generate one line error message
# instead of many compile error messages, if neither crossterm nor termion are selected.
feature_crossterm_or_termion_must_be_selected = []
crossterm = ["ratatui/crossterm", "feature_crossterm_or_termion_must_be_selected"]
termion = ["ratatui/termion", "feature_crossterm_or_termion_must_be_selected"]
formatter = []
# Docs.rs-specific configuration required to enable documentation of
# code requiring optional features.
[package.metadata.docs.rs]
# Document all features
all-features = true
# Defines the configuration attribute `docsrs`
rustdoc-args = ["--cfg", "docsrs"]
[[example]]
name="demo"
required-features=["feature_crossterm_or_termion_must_be_selected"]
================================================
FILE: DEV_NOTES.md
================================================
# Release process
## Prepare documentation
Run first `cargo rdme` and then decide on `cargo rdme --force`
## Update Changelog and Cargo.toml
Execute along the lines of:
1. `release-plz update`, then check in the versioned files.
2. `git push`
3. wait for github runners are completed
4. `git tag` and `git push --tags`
5. `cargo publish`
# Update demo.gif
In `doc` folder run `vhs demo.tape`.
Then rename `demo.gif` to current version and update Readme - currently via lib.rs
There is another `demo-short.tape`, which is used for the demo in ratatui website.
# Needed tools on macos
```sh
cargo install release-plz
cargo install cargo-rdme
brew install vhs
```
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2024 Jochen Kiemes
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# tui-logger
<!-- cargo-rdme start -->
## Logger with smart widget for the `tui` and `ratatui` crate
[](https://deps.rs/repo/github/gin66/tui-logger)

### Demo of the widget

### Documentation
[Documentation](https://docs.rs/tui-logger/latest/tui_logger/)
I have stumbled over an excellent AI-generated description of `tui-logger`, which provides surprisingly deep and (mostly) correct implementation details.
It would have costed me many days to write an equally good description with so many details and diagrams.
This docu can be found [here](https://deepwiki.com/gin66/tui-logger).
### Important note for `tui`
The `tui` crate has been archived and `ratatui` has taken over.
In order to avoid supporting compatibility for an inactive crate,
the v0.9.x releases are the last to support `tui`. In case future bug fixes
are needed, the branch `tui_legacy` has been created to track changes to 0.9.x releases.
Starting with v0.10 `tui-logger` is `ratatui` only.
### Features
- [X] Logger implementation for the `log` crate
- [X] Logger enable/disable detection via hash table (avoid string compare)
- [X] Hot logger code only copies enabled log messages with timestamp into a circular buffer
- [X] Widgets/move_message() retrieve captured log messages from hot circular buffer
- [X] Lost message detection due to circular buffer
- [X] Log filtering performed on log record target
- [X] Simple Widgets to view logs and configure debuglevel per target
- [X] Logging of enabled logs to file
- [X] Scrollback in log history
- [x] Title of target and log pane can be configured
- [X] `slog` support, providing a Drain to integrate into your `slog` infrastructure
- [X] `tracing` support
- [X] Support to use custom formatter for log events
- [X] Configurable by environment variables
- [ ] Allow configuration of target dependent loglevel specifically for file logging
- [X] Avoid duplicating of module_path and filename in every log record
- [ ] Simultaneous modification of all targets' display/hot logging loglevel by key command
### Smart Widget
Smart widget consists of two widgets. Left is the target selector widget and
on the right side the logging messages view scrolling up. The target selector widget
can be hidden/shown during runtime via key command.
The key command to be provided to the TuiLoggerWidget via transition() function.
The target selector widget looks like this:

It controls:
- Capturing of log messages by the logger
- Selection of levels for display in the logging message view
The two columns have the following meaning:
- Code EWIDT: E stands for Error, W for Warn, Info, Debug and Trace.
+ Inverted characters (EWIDT) are enabled log levels in the view
+ Normal characters show enabled capturing of a log level per target
+ If any of EWIDT are not shown, then the respective log level is not captured
- Target of the log events can be defined in the log e.g. `warn!(target: "demo", "Log message");`
### Smart Widget Key Commands
```rust
| KEY | ACTION
|----------|-----------------------------------------------------------|
| h | Toggles target selector widget hidden/visible
| f | Toggle focus on the selected target only
| UP | Select previous target in target selector widget
| DOWN | Select next target in target selector widget
| LEFT | Reduce SHOWN (!) log messages by one level
| RIGHT | Increase SHOWN (!) log messages by one level
| - | Reduce CAPTURED (!) log messages by one level
| + | Increase CAPTURED (!) log messages by one level
| PAGEUP | Enter Page Mode and scroll approx. half page up in log history.
| PAGEDOWN | Only in page mode: scroll 10 events down in log history.
| ESCAPE | Exit page mode and go back to scrolling mode
| SPACE | Toggles hiding of targets, which have logfilter set to off
```
The mapping of key to action has to be done in the application. The respective TuiWidgetEvent
has to be provided to TuiWidgetState::transition().
Remark to the page mode: The timestamp of the event at event history's bottom line is used as
reference. This means, changing the filters in the EWIDT/focus from the target selector window
should work as expected without jumps in the history. The page next/forward advances as
per visibility of the events.
### Basic usage to initialize logger-system:
```rust
#[macro_use]
extern crate log;
//use tui_logger;
fn main() {
// Early initialization of the logger
// Set max_log_level to Trace
tui_logger::init_logger(log::LevelFilter::Trace).unwrap();
// Set default level for unknown targets to Trace
tui_logger::set_default_level(log::LevelFilter::Trace);
// code....
}
```
For use of the widget please check examples/demo.rs
### Demo
Run demo using termion:
```rust
cargo run --example demo --features termion
```
Run demo with crossterm:
```rust
cargo run --example demo --features crossterm
```
Run demo using termion and simple custom formatter in bottom right log widget:
```rust
cargo run --example demo --features termion,formatter
```
### Configuration by environment variables
`tui.logger` uses `env-filter` crate to support configuration by a string or an environment variable.
This is an opt-in by call to one of these two functions.
```rust
pub fn set_env_filter_from_string(filterstring: &str) {}
pub fn set_env_filter_from_env(env_name: Option<&str>) {}
```
Default environment variable name is `RUST_LOG`.
### `slog` support
`tui-logger` provides a [`TuiSlogDrain`] which implements `slog::Drain` and will route all records
it receives to the `tui-logger` widget.
Enabled by feature "slog-support"
### `tracing-subscriber` support
`tui-logger` provides a [`TuiTracingSubscriberLayer`] which implements
`tracing_subscriber::Layer` and will collect all events
it receives to the `tui-logger` widget
Enabled by feature "tracing-support"
### Custom filtering
```rust
#[macro_use]
extern crate log;
//use tui_logger;
use env_logger;
fn main() {
// Early initialization of the logger
let drain = tui_logger::Drain::new();
// instead of tui_logger::init_logger, we use `env_logger`
env_logger::Builder::default()
.format(move |buf, record|
// patch the env-logger entry through our drain to the tui-logger
Ok(drain.log(record))
).init(); // make this the global logger
// code....
}
```
### Custom formatting
For experts only ! Configure along the lines:
```rust
use tui_logger::LogFormatter;
let formatter = MyLogFormatter();
TuiLoggerWidget::default()
.block(Block::bordered().title("Filtered TuiLoggerWidget"))
.formatter(formatter)
.state(&filter_state)
.render(left, buf);
```
The example demo can be invoked to use a custom formatter as example for the bottom right widget.
<!-- cargo-rdme end -->
### Internals
For logging there are two circular buffers in use:
* "hot" buffer, which is written to during any logging macro invocation
* main buffer, which holds events to be displayed by the widgets.
The size of the "hot" buffer is 1000 and can be modified by `set_hot_buffer_depth()`.
The size of the main buffer is 10000 and can be modified by `set_buffer_depth()`.
Reason for this scheme: The main buffer is locked for a while during widget updates.
In order to avoid blocking the log-macros, this scheme is in use.
The copy from "hot" buffer to main buffer is performed by a call to `move_events()`,
which is done in a cyclic task, which repeats every 10 ms, or when the hot buffer is half full.
In versions <0.13 log messages may have been lost, if the widget wasn't drawn.
```mermaid
flowchart LR
Logging["Logging Macros"] --> Capture["CAPTURE Filter"] --> HotBuffer["Hot Buffer (1000 entries)"]
MoveEvents["move_events()"]
HotBuffer --> MoveEvents
MoveEvents --> MainBuffer["Main Buffer (10000 entries)"]
MainBuffer --- Show1["SHOW Filter"] --- Widget1["Widget 1"]
MainBuffer --- Show2["SHOW Filter"] --- Widget2["Widget 2"]
MainBuffer --- ShowN["SHOW Filter"] --- Widget3["Widget N"]
Config1["set_hot_buffer_depth()"] -.-> HotBuffer
Config2["set_buffer_depth()"] -.-> MainBuffer
subgraph Triggers["Triggers"]
direction TB
T1["Every 10ms"]
T2["Hot buffer 50% full"]
end
Triggers -.-> MoveEvents
note["Note: Main buffer locks during widget updates"]
note -.-> MainBuffer
```
### THANKS TO
* [Florian Dehau](https://github.com/fdehau) for his great crate [tui-rs](https://github.com/fdehau/tui-rs)
* [Antoine Büsch](https://github.com/abusch) for providing the patches to tui-rs v0.3.0 and v0.6.0
* [Adam Sypniewski](https://github.com/ajsyp) for providing the patches to tui-rs v0.6.2
* [James aka jklong](https://github.com/jklong) for providing the patch to tui-rs v0.7
* [icy-ux](https://github.com/icy-ux) for adding slog support and example
* [alvinhochun](https://github.com/alvinhochun) for updating to tui 0.10 and crossterm support
* [brooksmtownsend](https://github.com/brooksmtownsend) Patch to remove verbose timestamp info
* [Kibouo](https://github.com/Kibouo) Patch to change Rc/Refcell to thread-safe counterparts
* [Afonso Bordado](https://github.com/afonso360) for providing the patch to tui-rs v0.17
* [Benjamin Kampmann](https://github.com/gnunicorn) for providing patch to tui-rs v0.18
* [Paul Sanders](https://github.com/pms1969) for providing patch in [issue #30](https://github.com/gin66/tui-logger/issues/30)
* [Ákos Hadnagy](https://github.com/ahadnagy) for providing patch in [#31](https://github.com/gin66/tui-logger/issues/31) for tracing-subscriber support
* [Orhun Parmaksız](https://github.com/orhun) for providing patches in [#33](https://github.com/gin66/tui-logger/issues/33), [#34](https://github.com/gin66/tui-logger/issues/34), [#37](https://github.com/gin66/tui-logger/issues/37) and [#46](https://github.com/gin66/tui-logger/issues/46)
* [purephantom](https://github.com/purephantom) for providing patch in [#42](https://github.com/gin66/tui-logger/issues/42) for ratatui update
* [Badr Bouslikhin](https://github.com/badrbouslikhin) for providing patch in [#47](https://github.com/gin66/tui-logger/issues/47) for ratatui update
* [ganthern](https://github.com/ganthern) for providing patch in [#49](https://github.com/gin66/tui-logger/issues/49) for tui support removal
* [Linda_pp](https://github.com/rhysd) for providing patch in [#51](https://github.com/gin66/tui-logger/issues/51) for Cell:set_symbol
* [Josh McKinney](https://github.com/joshka) for providing patch in
[#56](https://github.com/gin66/tui-logger/issues/56) for Copy on TuiWidgetEvent and
TuiLoggerWidget
* [nick42d](https://github.com/nick42d) for providing patch in
[#63](https://github.com/gin66/tui-logger/issues/63) for semver checks, [#74](https://github.com/gin66/tui-logger/pull/74) and [#87](https://github.com/gin66/tui-logger/issues/87)
* [Tom Groenwoldt](https://github.com/tomgroenwoldt) for providing patch in [#65](https://github.com/gin66/tui-logger/issues/65) for ratatui update
* [Kevin](https://github.com/doesnotcompete) for providing patch in [#71](https://github.com/issues/71)
* [urizennnn](https://github.com/urizennnn) for providing patch in [#72](https://github.com/issues/72)
* [Earthgames](https://github.com/Earthgames) for providing patch in [#84](https://github.com/issues/84) to fix panic for unicode characters
### Star History
[](https://star-history.com/#gin66/tui-logger&Date)
License: MIT
================================================
FILE: bacon.toml
================================================
# This is a configuration file for the bacon tool
#
# Bacon repository: https://github.com/Canop/bacon
# Complete help on configuration: https://dystroy.org/bacon/config/
# You can also check bacon's own bacon.toml file
# as an example: https://github.com/Canop/bacon/blob/main/bacon.toml
default_job = "check"
[jobs.check]
command = ["cargo", "check", "--color", "always"]
need_stdout = false
# This is a helpful job to check that the demo compiles with the crossterm
# feature enabled. This and the termion feature are mutually exclusive.
[jobs.check-crossterm]
command = ["cargo", "check", "--all-targets", "--features", "crossterm", "--color", "always"]
need_stdout = false
# This is a helpful job to check that the demo compiles with the termion
# feature enabled. This and the crossterm feature are mutually exclusive.
[jobs.check-termion]
command = ["cargo", "check", "--all-targets", "--features", "termion", "--color", "always"]
need_stdout = false
[jobs.clippy]
command = [
"cargo", "clippy",
"--color", "always",
]
need_stdout = false
[jobs.test]
command = [
"cargo", "test", "--libs", "--color", "always",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
]
need_stdout = true
[jobs.doc]
command = ["cargo", "doc", "--color", "always", "--no-deps"]
need_stdout = false
# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change
# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
1 = "job:check-crossterm"
2 = "job:check-termion"
================================================
FILE: doc/demo-short.tape
================================================
# VHS documentation
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
# Set Theme <json|string> Set the theme of the terminal
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
# Set BorderRadius <number> Set terminal border radius, in pixels.
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Escape[@<time>] [number] Press the Escape key
# Backspace[@<time>] [number] Press the Backspace key
# Delete[@<time>] [number] Press the Delete key
# Insert[@<time>] [number] Press the Insert key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# PageUp[@<time>] [number] Press the Page Up key
# PageDown[@<time>] [number] Press the Page Down key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
Output target/demo.gif
Set Theme "Aardvark Blue"
Set Width 1600
Set Height 1200
Set Framerate 50
Hide
Type "cargo run --example=demo --features=crossterm" Enter
Sleep 2s # allows time for build
Show
Right 2 # set App to tracing
Down 2 # select the crossterm target
Right 2 # set crossterm to tracing
Sleep 1s
# a screenshot for using in a more static context (e.g. ratatui.rs showcase)
Screenshot target/demo.png
Sleep 1s
# The following commented out commands are for a more full demo similar to the existing demo
# However they produce a much larger gif and are not necessary for the showcase
# Type "f" Sleep 2s # focus on the crossterm target
# Up
# Up
# Type "f" Sleep 2s # unfocus
# # unselect everything except error
# Left 4 Down
# Left 2 Down
# Left 4 Down
# Left 2 Down
# Left 2 Sleep 2s
# # Hide / show the selector
# Type "h" Sleep 2s
# Type "h" Sleep 2s
# # turn off tracing capture
# Type "-" Up
# Type "-" Up
# Type "-" Up
# Type "-" Up
# Type "-" Sleep 2s
Hide
Type "q"
================================================
FILE: doc/demo.tape
================================================
# VHS documentation
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
# Set Theme <json|string> Set the theme of the terminal
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
# Set BorderRadius <number> Set terminal border radius, in pixels.
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Escape[@<time>] [number] Press the Escape key
# Backspace[@<time>] [number] Press the Backspace key
# Delete[@<time>] [number] Press the Delete key
# Insert[@<time>] [number] Press the Insert key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# PageUp[@<time>] [number] Press the Page Up key
# PageDown[@<time>] [number] Press the Page Down key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
Output demo.gif
Require echo
Set Shell "bash"
# This gives terminal 50 heigth and 100 wide
Set FontSize 12
Set Width 900
Set Height 840
#Type "stty size" Sleep 100ms Enter
# Setup split screen, hide tmux bar with host name and simple prompt without user name
# zsh does not like comment lines and do not use history, so use sh
Hide
Type "tmux" Enter
Sleep 100ms
Type "tmux split-window -l 40" Sleep 100ms Enter
Type "/bin/sh" Enter
Type "export PS1='> '" Sleep 100ms Enter
Type "reset" Sleep 100ms Enter
Ctrl+B
Type ":set status off" Enter
Ctrl+B Type "o" Sleep 100ms
Type "/bin/sh" Enter
Type "export PS1=''" Sleep 100ms Enter
Type "reset" Sleep 100ms Enter
# Ready to show the windows
Show
# Currently we are in the top pane
Sleep 1s
Type "# run the demo" Sleep 100ms Enter
Ctrl+B Type "o" Sleep 100ms
Type "cargo run -q --example demo --features=termion,formatter" Enter
Sleep 2
Ctrl+B Type "o" Sleep 100ms
Type "# <States> at top are four tabs" Enter
Type "# In the middle is a smart widget with a target selector on the left" Enter
Type "# In the bottom left is a filtered tui logger widget" Enter
Type "# Bottom right is unfiltered tui logger widget with custom formatter ... MAYDAY" Enter
Type "# At the bottom is a progress task" Enter
Ctrl+B Type "o" Sleep 100ms
Sleep 1
Ctrl+B Type "o" Sleep 100ms
Type "# select the target <background-task2> with cursor down/up" Enter
Ctrl+B Type "o" Sleep 100ms
Sleep 5
Down Sleep 1
Down Sleep 1
Sleep 1
Ctrl+B Type "o" Sleep 100ms
Type "# focus on this target with f" Enter
Ctrl+B Type "o" Sleep 100ms
Type "f"
Sleep 5
Ctrl+B Type "o" Sleep 100ms
Type "# focus on the previous target with Cursor up" Enter
Ctrl+B Type "o" Sleep 100ms
Up Sleep 1
Ctrl+B Type "o" Sleep 100ms
Type "# Remove focus with f" Enter
Ctrl+B Type "o" Sleep 100ms
Type "f"
Sleep 5
Ctrl+B Type "o" Sleep 100ms
Type "# For all targets only show Errors using Cursor Left/Down" Enter
Ctrl+B Type "o" Sleep 100ms
Up Sleep 100ms
Left Sleep 100ms
Left Sleep 100ms
Down Sleep 100ms
Left Sleep 100ms
Left Sleep 100ms
Down Sleep 100ms
Left Sleep 100ms
Left Sleep 100ms
Down Sleep 100ms
Left Sleep 100ms
Left Sleep 100ms
Down Sleep 100ms
Left Sleep 100ms
Left Sleep 100ms
Sleep 5
Ctrl+B Type "o" Sleep 100ms
Type "# Disable recording of target trace with -" Enter
Ctrl+B Type "o" Sleep 100ms
Type "-" Sleep 100ms
Up
Type "-" Sleep 100ms
Up
Type "-" Sleep 100ms
Up
Type "-" Sleep 100ms
Up
Type "-" Sleep 100ms
Ctrl+B Type "o" Sleep 100ms
Type "# Hide/Unhide the selector pane with h" Enter
Ctrl+B Type "o" Sleep 100ms
Type "h"
Sleep 3
Type "h"
Sleep 3
Ctrl+B Type "o" Sleep 100ms
Type "# Switch to another Tab - independent Widget" Enter
Type "# Notice the disabled recording of target trace" Enter
Ctrl+B Type "o" Sleep 100ms
Tab
Sleep 5
Ctrl+B Type "o" Sleep 100ms
Type "# Quit with q" Enter
Ctrl+B Type "o" Sleep 100ms
Type "q"
Sleep 2
================================================
FILE: examples/demo.rs
================================================
use std::{io, sync::mpsc, thread, time};
use log::*;
use ratatui::{prelude::*, widgets::*};
use std::env;
use tui_logger::*;
/// Choose the backend depending on the selected feature (crossterm or termion). This is a mutually
/// exclusive feature, so only one of them can be enabled at a time.
#[cfg(all(feature = "crossterm", not(feature = "termion")))]
use self::crossterm_backend::*;
#[cfg(all(feature = "termion", not(feature = "crossterm")))]
use self::termion_backend::*;
#[cfg(not(any(feature = "crossterm", feature = "termion")))]
compile_error!("One of the features 'crossterm' or 'termion' must be enabled.");
#[cfg(all(feature = "crossterm", feature = "termion"))]
compile_error!("Only one of the features 'crossterm' and 'termion' can be enabled.");
struct App {
mode: AppMode,
states: Vec<TuiWidgetState>,
tab_names: Vec<&'static str>,
selected_tab: usize,
progress_counter: Option<u16>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum AppMode {
#[default]
Run,
Quit,
}
#[derive(Debug)]
enum AppEvent {
UiEvent(Event),
CounterChanged(Option<u16>),
}
//// Example for simple customized formatter
struct MyLogFormatter {}
impl LogFormatter for MyLogFormatter {
fn min_width(&self) -> u16 {
4
}
fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
let mut lines = vec![];
match evt.level {
log::Level::Error => {
let st = Style::new().red().bold();
let sp = Span::styled("======", st);
let mayday = Span::from(" MAYDAY MAYDAY ".to_string());
let sp2 = Span::styled("======", st);
lines.push(Line::from(vec![sp, mayday, sp2]).alignment(Alignment::Center));
lines.push(
Line::from(format!("{}: {}", evt.level, evt.msg()))
.alignment(Alignment::Center),
);
}
_ => {
lines.push(Line::from(format!("{}: {}", evt.level, evt.msg())));
}
};
match evt.level {
log::Level::Error => {
let st = Style::new().blue().bold();
let sp = Span::styled("======", st);
let mayday = Span::from(" MAYDAY SEEN ? ".to_string());
let sp2 = Span::styled("======", st);
lines.push(Line::from(vec![sp, mayday, sp2]).alignment(Alignment::Center));
}
_ => {}
};
lines
}
}
fn main() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Trace);
let mut dir = env::temp_dir();
dir.push("tui-logger_demo.log");
let file_options = TuiLoggerFile::new(dir.to_str().unwrap())
.output_level(Some(TuiLoggerLevelOutput::Abbreviated))
.output_file(false)
.output_separator(':');
set_log_file(file_options);
debug!(target:"App", "Logging to {}", dir.to_str().unwrap());
debug!(target:"App", "Logging initialized");
let mut terminal = init_terminal().unwrap();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
App::new().start(&mut terminal).unwrap();
restore_terminal().unwrap();
terminal.clear().unwrap();
}
impl App {
pub fn new() -> App {
let states = vec![
TuiWidgetState::new().set_default_display_level(LevelFilter::Info),
TuiWidgetState::new().set_default_display_level(LevelFilter::Info),
TuiWidgetState::new().set_default_display_level(LevelFilter::Info),
TuiWidgetState::new().set_default_display_level(LevelFilter::Info),
];
// Adding this line had provoked the bug as described in issue #69
// let states = states.into_iter().map(|s| s.set_level_for_target("some::logger", LevelFilter::Off)).collect();
let tab_names = vec!["State 1", "State 2", "State 3", "State 4"];
App {
mode: AppMode::Run,
states,
tab_names,
selected_tab: 0,
progress_counter: None,
}
}
pub fn start<B: Backend>(mut self, terminal: &mut Terminal<B>) -> Result<(), B::Error> {
// Use an mpsc::channel to combine stdin events with app events
let (tx, rx) = mpsc::channel();
let event_tx = tx.clone();
let progress_tx = tx.clone();
thread::spawn(move || input_thread(event_tx).unwrap());
thread::spawn(move || progress_task(progress_tx).unwrap());
thread::spawn(move || background_task());
thread::spawn(move || background_task2());
thread::spawn(move || heart_task());
self.run(terminal, rx)
}
/// Main application loop
fn run<B: Backend>(
&mut self,
terminal: &mut Terminal<B>,
rx: mpsc::Receiver<AppEvent>,
) -> Result<(), B::Error> {
for event in rx {
match event {
AppEvent::UiEvent(event) => self.handle_ui_event(event),
AppEvent::CounterChanged(value) => self.update_progress_bar(event, value),
}
if self.mode == AppMode::Quit {
break;
}
self.draw(terminal)?;
}
Ok(())
}
fn update_progress_bar(&mut self, event: AppEvent, value: Option<u16>) {
trace!(target: "App", "Updating progress bar {:?}",event);
self.progress_counter = value;
if value.is_none() {
info!(target: "App", "Background task finished");
}
}
fn handle_ui_event(&mut self, event: Event) {
debug!(target: "App", "Handling UI event: {:?}",event);
let state = self.selected_state();
if let Event::Key(key) = event {
#[cfg(feature = "crossterm")]
let code = key.code;
#[cfg(feature = "termion")]
let code = key;
match code.into() {
Key::Char('q') => self.mode = AppMode::Quit,
Key::Char('\t') => self.next_tab(),
#[cfg(feature = "crossterm")]
Key::Tab => self.next_tab(),
Key::Char(' ') => state.transition(TuiWidgetEvent::SpaceKey),
Key::Esc => state.transition(TuiWidgetEvent::EscapeKey),
Key::PageUp => state.transition(TuiWidgetEvent::PrevPageKey),
Key::PageDown => state.transition(TuiWidgetEvent::NextPageKey),
Key::Up => state.transition(TuiWidgetEvent::UpKey),
Key::Down => state.transition(TuiWidgetEvent::DownKey),
Key::Left => state.transition(TuiWidgetEvent::LeftKey),
Key::Right => state.transition(TuiWidgetEvent::RightKey),
Key::Char('+') => state.transition(TuiWidgetEvent::PlusKey),
Key::Char('-') => state.transition(TuiWidgetEvent::MinusKey),
Key::Char('h') => state.transition(TuiWidgetEvent::HideKey),
Key::Char('f') => state.transition(TuiWidgetEvent::FocusKey),
_ => (),
}
}
}
fn selected_state(&mut self) -> &mut TuiWidgetState {
&mut self.states[self.selected_tab]
}
fn next_tab(&mut self) {
self.selected_tab = (self.selected_tab + 1) % self.tab_names.len();
}
fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<(), B::Error> {
terminal.draw(|frame| {
frame.render_widget(self, frame.area());
})?;
Ok(())
}
}
/// A simulated task that sends a counter value to the UI ranging from 0 to 100 every second.
fn progress_task(tx: mpsc::Sender<AppEvent>) -> anyhow::Result<()> {
for progress in 0..100 {
debug!(target:"progress-task", "Send progress to UI thread. Value: {:?}", progress);
tx.send(AppEvent::CounterChanged(Some(progress)))?;
trace!(target:"progress-task", "Sleep one second");
thread::sleep(time::Duration::from_millis(1000));
}
info!(target:"progress-task", "Progress task finished");
tx.send(AppEvent::CounterChanged(None))?;
Ok(())
}
/// A background task that logs a log entry for each log level every second.
fn background_task() {
loop {
error!(target:"background-task", "an error");
warn!(target:"background-task", "a warning");
info!(target:"background-task", "a two line info\nsecond line");
debug!(target:"background-task", "a debug");
trace!(target:"background-task", "a trace");
thread::sleep(time::Duration::from_millis(1000));
}
}
/// A background task for long line
fn background_task2() {
loop {
info!(target:"background-task2", "This is a very long message, which should be wrapped on smaller screen by the standard formatter with an indentation of 9 characters.");
thread::sleep(time::Duration::from_millis(2000));
}
}
/// A background task for utf8 example
fn heart_task() {
let mut line = "♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥♥".to_string();
loop {
info!(target:"heart-task", "{}", line);
line = format!(".{}", line);
thread::sleep(time::Duration::from_millis(1500));
}
}
impl Widget for &mut App {
fn render(self, area: Rect, buf: &mut Buffer) {
let progress_height = if self.progress_counter.is_some() {
3
} else {
0
};
let [tabs_area, smart_area, main_area, progress_area, help_area] = Layout::vertical([
Constraint::Length(3),
Constraint::Fill(50),
Constraint::Fill(30),
Constraint::Length(progress_height),
Constraint::Length(3),
])
.areas(area);
// show two TuiWidgetState side-by-side
let [left, right] = Layout::horizontal([Constraint::Fill(1); 2]).areas(main_area);
Tabs::new(self.tab_names.iter().cloned())
.block(Block::default().title("States").borders(Borders::ALL))
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.select(self.selected_tab)
.render(tabs_area, buf);
TuiLoggerSmartWidget::default()
.style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow))
.style_trace(Style::default().fg(Color::Magenta))
.style_info(Style::default().fg(Color::Cyan))
.output_separator(':')
.output_timestamp(Some("%H:%M:%S".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Abbreviated))
.output_target(true)
.output_file(true)
.output_line(true)
.state(self.selected_state())
.render(smart_area, buf);
// An example of filtering the log output. The left TuiLoggerWidget is filtered to only show
// log entries for the "App" target. The right TuiLoggerWidget shows all log entries.
let filter_state = TuiWidgetState::new()
.set_default_display_level(LevelFilter::Off)
.set_level_for_target("App", LevelFilter::Debug)
.set_level_for_target("background-task", LevelFilter::Info);
let mut formatter: Option<Box<dyn LogFormatter>> = None;
if cfg!(feature = "formatter") {
formatter = Some(Box::new(MyLogFormatter {}));
}
TuiLoggerWidget::default()
.block(Block::bordered().title("Filtered TuiLoggerWidget"))
.output_separator('|')
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Long))
.output_target(false)
.output_file(false)
.output_line(false)
.style(Style::default().fg(Color::White))
.state(&filter_state)
.render(left, buf);
TuiLoggerWidget::default()
.block(Block::bordered().title("Unfiltered TuiLoggerWidget"))
.opt_formatter(formatter)
.output_separator('|')
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
.output_level(Some(TuiLoggerLevelOutput::Long))
.output_target(false)
.output_file(false)
.output_line(false)
.style(Style::default().fg(Color::White))
.render(right, buf);
if let Some(percent) = self.progress_counter {
Gauge::default()
.block(Block::bordered().title("progress-task"))
.gauge_style((Color::White, Modifier::ITALIC))
.percent(percent)
.render(progress_area, buf);
}
if area.width > 40 {
Text::from(vec![
"Q: Quit | Tab: Switch state | ↑/↓: Select target | f: Focus target".into(),
"←/→: Display level | +/-: Filter level | Space: Toggle hidden targets".into(),
"h: Hide target selector | PageUp/Down: Scroll | Esc: Cancel scroll".into(),
])
.style(Color::Gray)
.centered()
.render(help_area, buf);
}
}
}
/// A module for crossterm specific code
#[cfg(feature = "crossterm")]
mod crossterm_backend {
use super::*;
pub use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode as Key},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
trace!(target:"crossterm", "Initializing terminal");
enable_raw_mode()?;
execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(io::stdout());
Terminal::new(backend)
}
pub fn restore_terminal() -> io::Result<()> {
trace!(target:"crossterm", "Restoring terminal");
disable_raw_mode()?;
execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture)
}
pub fn input_thread(tx_event: mpsc::Sender<AppEvent>) -> anyhow::Result<()> {
trace!(target:"crossterm", "Starting input thread");
while let Ok(event) = event::read() {
trace!(target:"crossterm", "Stdin event received {:?}", event);
tx_event.send(AppEvent::UiEvent(event))?;
}
Ok(())
}
}
/// A module for termion specific code
#[cfg(feature = "termion")]
mod termion_backend {
use super::*;
use termion::screen::IntoAlternateScreen;
pub use termion::{
event::{Event, Key},
input::{MouseTerminal, TermRead},
raw::IntoRawMode,
};
pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
trace!(target:"termion", "Initializing terminal");
let stdout = io::stdout().into_raw_mode()?;
let stdout = MouseTerminal::from(stdout);
let stdout = stdout.into_alternate_screen()?;
let backend = TermionBackend::new(stdout);
Terminal::new(backend)
}
pub fn restore_terminal() -> io::Result<()> {
trace!(target:"termion", "Restoring terminal");
Ok(())
}
pub fn input_thread(tx_event: mpsc::Sender<AppEvent>) -> anyhow::Result<()> {
trace!(target:"termion", "Starting input thread");
for event in io::stdin().events() {
let event = event?;
trace!(target:"termion", "Stdin event received {:?}", event);
tx_event.send(AppEvent::UiEvent(event))?;
}
Ok(())
}
}
================================================
FILE: examples/slog.rs_outdated
================================================
//! Demo of slog-based logging
//!
use slog::{debug, error, info, o, trace, warn, Drain, Logger};
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
use std::sync::mpsc;
use std::{thread, time};
use termion::event::{self, Key};
use termion::input::{MouseTerminal, TermRead};
use termion::raw::IntoRawMode;
use termion::screen::AlternateScreen;
use tui::backend::{Backend, TermionBackend};
use tui::layout::{Constraint, Direction, Layout, Rect};
use tui::style::{Color, Modifier, Style};
use tui::text::Spans;
use tui::widgets::{Block, Borders, Gauge, Tabs};
use tui::Frame;
use tui::Terminal;
use tui_logger::*;
struct App {
states: Vec<TuiWidgetState>,
dispatcher: Rc<RefCell<Dispatcher<event::Event>>>,
selected_tab: Rc<RefCell<usize>>,
opt_info_cnt: Option<u16>,
}
#[derive(Debug)]
enum AppEvent {
Termion(termion::event::Event),
LoopCnt(Option<u16>),
}
fn demo_application(log: Logger, tx: mpsc::Sender<AppEvent>) {
let one_second = time::Duration::from_millis(1_000);
let mut lp_cnt = (1..=100).into_iter();
loop {
trace!(log, "Sleep one second");
thread::sleep(one_second);
trace!(log, "Issue log entry for each level");
error!(log, "an error");
warn!(log, "a warning");
trace!(log, "a trace");
debug!(log, "a debug");
info!(log, "an info");
tx.send(AppEvent::LoopCnt(lp_cnt.next())).unwrap();
}
}
fn main() -> std::result::Result<(), std::io::Error> {
let drain = tui_logger::slog_drain().fuse();
let log = slog::Logger::root(drain, o!());
info!(log, "Start demo");
let stdout = io::stdout().into_raw_mode().unwrap();
let stdout = MouseTerminal::from(stdout);
let stdout = AlternateScreen::from(stdout);
let backend = TermionBackend::new(stdout);
let mut terminal = Terminal::new(backend).unwrap();
let stdin = io::stdin();
terminal.clear().unwrap();
terminal.hide_cursor().unwrap();
// Use an mpsc::channel to combine stdin events with app events
let (tx, rx) = mpsc::channel();
let tx_event = tx.clone();
let log_thread = log.clone();
thread::spawn(move || {
for c in stdin.events() {
trace!(log_thread, "Stdin event received {:?}", c);
tx_event.send(AppEvent::Termion(c.unwrap())).unwrap();
}
});
let log_thread = log.clone();
thread::spawn(move || {
demo_application(log_thread, tx);
});
let mut app = App {
states: vec![],
dispatcher: Rc::new(RefCell::new(Dispatcher::<event::Event>::new())),
selected_tab: Rc::new(RefCell::new(0)),
opt_info_cnt: None,
};
let log_thread = log.clone();
// Here is the main loop
for evt in rx {
trace!(log_thread, "{:?}", evt);
match evt {
AppEvent::Termion(evt) => {
if !app.dispatcher.borrow_mut().dispatch(&evt) {
if evt == termion::event::Event::Key(event::Key::Char('q')) {
break;
}
}
}
AppEvent::LoopCnt(opt_cnt) => {
app.opt_info_cnt = opt_cnt;
}
}
terminal.draw(|mut f| {
let size = f.size();
draw_frame(&mut f, size, &mut app);
})?;
}
terminal.show_cursor().unwrap();
terminal.clear().unwrap();
Ok(())
}
fn draw_frame<B: Backend>(t: &mut Frame<B>, size: Rect, app: &mut App) {
let tabs = vec!["V1", "V2", "V3", "V4"];
let tabs = tabs.into_iter().map(|t| Spans::from(t)).collect::<Vec<_>>();
let sel = *app.selected_tab.borrow();
let sel_tab = if sel + 1 < tabs.len() { sel + 1 } else { 0 };
let sel_stab = if sel > 0 { sel - 1 } else { tabs.len() - 1 };
let v_sel = app.selected_tab.clone();
// Switch between tabs via Tab and Shift-Tab
// At least on my computer the 27/91/90 equals a Shift-Tab
app.dispatcher.borrow_mut().clear();
app.dispatcher.borrow_mut().add_listener(move |evt| {
if &event::Event::Unsupported(vec![27, 91, 90]) == evt {
*v_sel.borrow_mut() = sel_stab;
true
} else if &event::Event::Key(Key::Char('\t')) == evt {
*v_sel.borrow_mut() = sel_tab;
true
} else {
false
}
});
if app.states.len() <= sel {
app.states.push(TuiWidgetState::new());
}
let block = Block::default().borders(Borders::ALL);
t.render_widget(block, size);
let mut constraints = vec![
Constraint::Length(3),
Constraint::Percentage(50),
Constraint::Min(3),
];
if app.opt_info_cnt.is_some() {
constraints.push(Constraint::Length(3));
}
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(size);
let tabs = Tabs::new(tabs)
.block(Block::default().borders(Borders::ALL))
.highlight_style(Style::default().add_modifier(Modifier::REVERSED))
.select(sel);
t.render_widget(tabs, chunks[0]);
let tui_sm = TuiLoggerSmartWidget::default()
.border_style(Style::default().fg(Color::Black))
.style_error(Style::default().fg(Color::Red))
.style_debug(Style::default().fg(Color::Green))
.style_warn(Style::default().fg(Color::Yellow))
.style_trace(Style::default().fg(Color::Magenta))
.style_info(Style::default().fg(Color::Cyan))
.state(&mut app.states[sel])
.dispatcher(app.dispatcher.clone());
t.render_widget(tui_sm, chunks[1]);
let tui_w: TuiLoggerWidget = TuiLoggerWidget::default()
.block(
Block::default()
.title("Independent Tui Logger View")
//.title_style(Style::default().fg(Color::White).bg(Color::Black))
.border_style(Style::default().fg(Color::White).bg(Color::Black))
.borders(Borders::ALL),
)
.style(Style::default().fg(Color::White).bg(Color::Black));
t.render_widget(tui_w, chunks[2]);
if let Some(percent) = app.opt_info_cnt {
let guage = Gauge::default()
.block(Block::default().borders(Borders::ALL).title("Progress"))
.style(
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::ITALIC),
)
.percent(percent);
t.render_widget(guage, chunks[3]);
}
}
================================================
FILE: src/circular.rs
================================================
use std::iter;
/// CircularBuffer is used to store the last elements of an endless sequence.
/// Oldest elements will be overwritten. The implementation focus on
/// speed. So memory allocations are avoided.
///
/// Usage example:
///```
/// extern crate tui_logger;
///
/// use tui_logger::CircularBuffer;
///
/// let mut cb : CircularBuffer<u64> = CircularBuffer::new(5);
/// cb.push(1);
/// cb.push(2);
/// cb.push(3);
/// cb.push(4);
/// cb.push(5);
/// cb.push(6); // This will overwrite the first element
///
/// // Total elements pushed into the buffer is 6.
/// assert_eq!(6,cb.total_elements());
///
/// // Thus the buffer has wrapped around.
/// assert_eq!(true,cb.has_wrapped());
///
/// /// Iterate through the elements:
/// {
/// let mut iter = cb.iter();
/// assert_eq!(Some(&2), iter.next());
/// assert_eq!(Some(&3), iter.next());
/// assert_eq!(Some(&4), iter.next());
/// assert_eq!(Some(&5), iter.next());
/// assert_eq!(Some(&6), iter.next());
/// assert_eq!(None, iter.next());
/// }
///
/// /// Iterate backwards through the elements:
/// {
/// let mut iter = cb.rev_iter();
/// assert_eq!(Some(&6), iter.next());
/// assert_eq!(Some(&5), iter.next());
/// assert_eq!(Some(&4), iter.next());
/// assert_eq!(Some(&3), iter.next());
/// assert_eq!(Some(&2), iter.next());
/// assert_eq!(None, iter.next());
/// }
///
/// // The elements in the buffer are now:
/// assert_eq!(vec![2,3,4,5,6],cb.take());
///
/// // After taking all elements, the buffer is empty.
/// let now_empty : Vec<u64> = vec![];
/// assert_eq!(now_empty,cb.take());
///```
pub struct CircularBuffer<T> {
buffer: Vec<T>,
next_write_pos: usize,
}
#[allow(dead_code)]
impl<T> CircularBuffer<T> {
/// Create a new CircularBuffer, which can hold max_depth elements
pub fn new(max_depth: usize) -> CircularBuffer<T> {
CircularBuffer {
buffer: Vec::with_capacity(max_depth),
next_write_pos: 0,
}
}
/// Return the number of elements present in the buffer
pub fn len(&self) -> usize {
self.buffer.len()
}
pub fn is_empty(&self) -> bool {
self.buffer.is_empty()
}
pub fn capacity(&self) -> usize {
self.buffer.capacity()
}
/// Next free index of the buffer
pub fn first_index(&self) -> Option<usize> {
if self.next_write_pos == 0 {
None
} else if self.next_write_pos < self.buffer.capacity() {
Some(0)
} else {
Some(self.next_write_pos - self.buffer.capacity())
}
}
pub fn last_index(&self) -> Option<usize> {
if self.next_write_pos == 0 {
None
} else {
Some(self.next_write_pos - 1)
}
}
pub fn element_at_index(&self, index: usize) -> Option<&T> {
let max_depth = self.buffer.capacity();
if index >= self.next_write_pos {
return None;
}
if index + max_depth < self.next_write_pos {
return None;
}
Some(&self.buffer[index % max_depth])
}
/// Push a new element into the buffer.
/// Until the capacity is reached, elements are pushed.
/// Afterwards the oldest elements will be overwritten.
pub fn push(&mut self, elem: T) {
let max_depth = self.buffer.capacity();
if self.buffer.len() < max_depth {
self.buffer.push(elem);
} else {
self.buffer[self.next_write_pos % max_depth] = elem;
}
self.next_write_pos += 1;
}
/// Take out all elements from the buffer, leaving an empty buffer behind
pub fn take(&mut self) -> Vec<T> {
let mut consumed = vec![];
let max_depth = self.buffer.capacity();
if self.buffer.len() < max_depth {
consumed.append(&mut self.buffer);
} else {
let pos = self.next_write_pos % max_depth;
let mut xvec = self.buffer.split_off(pos);
consumed.append(&mut xvec);
consumed.append(&mut self.buffer)
}
self.next_write_pos = 0;
consumed
}
/// Total number of elements pushed into the buffer.
pub fn total_elements(&self) -> usize {
self.next_write_pos
}
/// If has_wrapped() is true, then elements have been overwritten
pub fn has_wrapped(&self) -> bool {
self.next_write_pos > self.buffer.capacity()
}
/// Return an iterator to step through all elements in the sequence,
/// as these have been pushed (FIFO)
pub fn iter(&mut self) -> iter::Chain<std::slice::Iter<'_, T>, std::slice::Iter<'_, T>> {
let max_depth = self.buffer.capacity();
if self.next_write_pos <= max_depth {
// If buffer is not completely filled, then just iterate through it
self.buffer.iter().chain(self.buffer[..0].iter())
} else {
let wrap = self.next_write_pos % max_depth;
let it_end = self.buffer[..wrap].iter();
let it_start = self.buffer[wrap..].iter();
it_start.chain(it_end)
}
}
/// Return an iterator to step through all elements in the reverse sequence,
/// as these have been pushed (LIFO)
pub fn rev_iter(
&mut self,
) -> iter::Chain<std::iter::Rev<std::slice::Iter<'_, T>>, std::iter::Rev<std::slice::Iter<'_, T>>>
{
let max_depth = self.buffer.capacity();
if self.next_write_pos <= max_depth {
// If buffer is not completely filled, then just iterate through it
self.buffer
.iter()
.rev()
.chain(self.buffer[..0].iter().rev())
} else {
let wrap = self.next_write_pos % max_depth;
let it_end = self.buffer[..wrap].iter().rev();
let it_start = self.buffer[wrap..].iter().rev();
it_end.chain(it_start)
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn circular_buffer() {
use crate::CircularBuffer;
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
// Empty buffer
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), None);
assert_eq!(cb.last_index(), None);
}
// Push in a value
cb.push(1);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(0));
assert_eq!(cb.last_index(), Some(0));
}
// Push in a value
cb.push(2);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(0));
assert_eq!(cb.last_index(), Some(1));
}
// Push in a value
cb.push(3);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(0));
assert_eq!(cb.last_index(), Some(2));
}
// Push in a value
cb.push(4);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(0));
assert_eq!(cb.last_index(), Some(3));
}
// Push in a value
cb.push(5);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(0));
assert_eq!(cb.last_index(), Some(4));
}
// Push in a value
cb.push(6);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(1));
assert_eq!(cb.last_index(), Some(5));
}
// Push in a value
cb.push(7);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(2));
assert_eq!(cb.last_index(), Some(6));
}
// Push in a value
cb.push(8);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(3));
assert_eq!(cb.last_index(), Some(7));
}
// Push in a value
cb.push(9);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(4));
assert_eq!(cb.last_index(), Some(8));
}
// Push in a value
cb.push(10);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), Some(&10));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(5));
assert_eq!(cb.last_index(), Some(9));
}
// Push in a value
cb.push(11);
{
let mut cb_iter = cb.iter();
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), Some(&10));
assert_eq!(cb_iter.next(), Some(&11));
assert_eq!(cb_iter.next(), None);
assert_eq!(cb.first_index(), Some(6));
assert_eq!(cb.last_index(), Some(10));
assert_eq!(cb.element_at_index(5), None);
assert_eq!(cb.element_at_index(6), Some(&7));
assert_eq!(cb.element_at_index(10), Some(&11));
assert_eq!(cb.element_at_index(11), None);
}
}
#[test]
fn circular_buffer_rev() {
use crate::CircularBuffer;
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
// Empty buffer
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(1);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(2);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(3);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(4);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(5);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), Some(&1));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(6);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), Some(&2));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(7);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), Some(&3));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(8);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), Some(&4));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(9);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), Some(&5));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(10);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&10));
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), Some(&6));
assert_eq!(cb_iter.next(), None);
}
// Push in a value
cb.push(11);
{
let mut cb_iter = cb.rev_iter();
assert_eq!(cb_iter.next(), Some(&11));
assert_eq!(cb_iter.next(), Some(&10));
assert_eq!(cb_iter.next(), Some(&9));
assert_eq!(cb_iter.next(), Some(&8));
assert_eq!(cb_iter.next(), Some(&7));
assert_eq!(cb_iter.next(), None);
}
}
#[test]
fn total_elements() {
use crate::CircularBuffer;
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
assert_eq!(0, cb.total_elements());
for i in 1..20 {
cb.push(i);
assert_eq!(i as usize, cb.total_elements());
}
}
#[test]
fn has_wrapped() {
use crate::CircularBuffer;
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
assert_eq!(0, cb.total_elements());
for i in 1..20 {
cb.push(i);
assert_eq!(i >= 6, cb.has_wrapped());
}
}
#[test]
fn take() {
use crate::CircularBuffer;
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
for i in 1..5 {
cb.push(i);
}
assert_eq!(vec![1, 2, 3, 4], cb.take());
for i in 1..6 {
cb.push(i);
}
assert_eq!(vec![1, 2, 3, 4, 5], cb.take());
for i in 1..7 {
cb.push(i);
}
assert_eq!(vec![2, 3, 4, 5, 6], cb.take());
let mut cb: CircularBuffer<u64> = CircularBuffer::new(5);
for i in 1..20 {
cb.push(i);
}
assert_eq!(vec![15, 16, 17, 18, 19], cb.take());
}
}
================================================
FILE: src/config/level_config.rs
================================================
use std::collections::hash_map::Iter;
use std::collections::hash_map::Keys;
use std::collections::HashMap;
use log::LevelFilter;
/// LevelConfig stores the relation target->LevelFilter in a hash table.
///
/// The table supports copying from the logger system LevelConfig to
/// a widget's LevelConfig. In order to detect changes, the generation
/// of the hash table is compared with any previous copied table.
/// On every change the generation is incremented.
#[derive(Default)]
pub struct LevelConfig {
config: HashMap<String, LevelFilter>,
generation: u64,
origin_generation: u64,
default_display_level: Option<LevelFilter>,
}
impl LevelConfig {
/// Create an empty LevelConfig.
pub fn new() -> LevelConfig {
LevelConfig {
config: HashMap::new(),
generation: 0,
origin_generation: 0,
default_display_level: None,
}
}
/// Set for a given target the LevelFilter in the table and update the generation.
pub fn set(&mut self, target: &str, level: LevelFilter) {
if let Some(lev) = self.config.get_mut(target) {
if *lev != level {
*lev = level;
self.generation += 1;
}
return;
}
self.config.insert(target.to_string(), level);
self.generation += 1;
}
/// Set default display level filter for new targets - independent from recording
pub fn set_default_display_level(&mut self, level: LevelFilter) {
self.default_display_level = Some(level);
}
/// Get default display level filter for new targets - independent from recording
pub fn get_default_display_level(&self) -> Option<LevelFilter> {
self.default_display_level
}
/// Retrieve an iter for all the targets stored in the hash table.
pub fn keys(&self) -> Keys<'_, String, LevelFilter> {
self.config.keys()
}
/// Get the levelfilter for a given target.
pub fn get(&self, target: &str) -> Option<LevelFilter> {
self.config.get(target).cloned()
}
/// Retrieve an iterator through all entries of the table.
pub fn iter(&self) -> Iter<'_, String, LevelFilter> {
self.config.iter()
}
/// Merge an origin LevelConfig into this one.
///
/// The origin table defines the maximum levelfilter.
/// If this table has a higher levelfilter, then it will be reduced.
/// Unknown targets will be copied to this table.
pub(crate) fn merge(&mut self, origin: &LevelConfig) {
if self.origin_generation != origin.generation {
for (target, origin_levelfilter) in origin.iter() {
if let Some(levelfilter) = self.get(target) {
if levelfilter <= *origin_levelfilter {
continue;
}
}
let levelfilter = self
.default_display_level
.map(|lvl| {
if lvl > *origin_levelfilter {
*origin_levelfilter
} else {
lvl
}
})
.unwrap_or(*origin_levelfilter);
self.set(target, levelfilter);
}
self.generation = origin.generation;
}
}
}
================================================
FILE: src/config/mod.rs
================================================
pub mod level_config;
pub use level_config::LevelConfig;
================================================
FILE: src/file.rs
================================================
use std::fs::{File, OpenOptions};
use crate::logger::TuiLoggerLevelOutput;
/// This closely follows the options of [``TuiLoggerSmartWidget``] but is used of logging to a file.
pub struct TuiLoggerFile {
pub(crate) dump: File,
pub(crate) format_separator: char,
pub(crate) timestamp_fmt: Option<String>,
pub(crate) format_output_target: bool,
pub(crate) format_output_file: bool,
pub(crate) format_output_line: bool,
pub(crate) format_output_level: Option<TuiLoggerLevelOutput>,
}
impl TuiLoggerFile {
pub fn new(fname: &str) -> Self {
TuiLoggerFile {
dump: OpenOptions::new()
.create(true)
.append(true)
.open(fname)
.expect("Failed to open dump File"),
format_separator: ':',
timestamp_fmt: Some("[%Y:%m:%d %H:%M:%S]".to_string()),
format_output_file: true,
format_output_line: true,
format_output_target: true,
format_output_level: Some(TuiLoggerLevelOutput::Long),
}
}
pub fn output_target(mut self, enabled: bool) -> Self {
self.format_output_target = enabled;
self
}
pub fn output_file(mut self, enabled: bool) -> Self {
self.format_output_file = enabled;
self
}
pub fn output_line(mut self, enabled: bool) -> Self {
self.format_output_line = enabled;
self
}
pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
self.timestamp_fmt = fmt;
self
}
pub fn output_separator(mut self, sep: char) -> Self {
self.format_separator = sep;
self
}
pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
self.format_output_level = level;
self
}
}
================================================
FILE: src/lib.rs
================================================
//! # Logger with smart widget for the `tui` and `ratatui` crate
//!
//! [](https://deps.rs/repo/github/gin66/tui-logger)
//! 
//!
//!
//! ## Demo of the widget
//!
//! 
//!
//! ## Documentation
//!
//! [Documentation](https://docs.rs/tui-logger/latest/tui_logger/)
//!
//! I have stumbled over an excellent AI-generated description of `tui-logger`, which provides surprisingly deep and (mostly) correct implementation details.
//! It would have costed me many days to write an equally good description with so many details and diagrams.
//! This docu can be found [here](https://deepwiki.com/gin66/tui-logger).
//!
//! ## Important note for `tui`
//!
//! The `tui` crate has been archived and `ratatui` has taken over.
//! In order to avoid supporting compatibility for an inactive crate,
//! the v0.9.x releases are the last to support `tui`. In case future bug fixes
//! are needed, the branch `tui_legacy` has been created to track changes to 0.9.x releases.
//!
//! Starting with v0.10 `tui-logger` is `ratatui` only.
//!
//! ## Features
//!
//! - [X] Logger implementation for the `log` crate
//! - [X] Logger enable/disable detection via hash table (avoid string compare)
//! - [X] Hot logger code only copies enabled log messages with timestamp into a circular buffer
//! - [X] Widgets/move_message() retrieve captured log messages from hot circular buffer
//! - [X] Lost message detection due to circular buffer
//! - [X] Log filtering performed on log record target
//! - [X] Simple Widgets to view logs and configure debuglevel per target
//! - [X] Logging of enabled logs to file
//! - [X] Scrollback in log history
//! - [x] Title of target and log pane can be configured
//! - [X] `slog` support, providing a Drain to integrate into your `slog` infrastructure
//! - [X] `tracing` support
//! - [X] Support to use custom formatter for log events
//! - [X] Configurable by environment variables
//! - [ ] Allow configuration of target dependent loglevel specifically for file logging
//! - [X] Avoid duplicating of module_path and filename in every log record
//! - [ ] Simultaneous modification of all targets' display/hot logging loglevel by key command
//!
//! ## Smart Widget
//!
//! Smart widget consists of two widgets. Left is the target selector widget and
//! on the right side the logging messages view scrolling up. The target selector widget
//! can be hidden/shown during runtime via key command.
//! The key command to be provided to the TuiLoggerWidget via transition() function.
//!
//! The target selector widget looks like this:
//!
//! 
//!
//! It controls:
//!
//! - Capturing of log messages by the logger
//! - Selection of levels for display in the logging message view
//!
//! The two columns have the following meaning:
//!
//! - Code EWIDT: E stands for Error, W for Warn, Info, Debug and Trace.
//! + Inverted characters (EWIDT) are enabled log levels in the view
//! + Normal characters show enabled capturing of a log level per target
//! + If any of EWIDT are not shown, then the respective log level is not captured
//! - Target of the log events can be defined in the log e.g. `warn!(target: "demo", "Log message");`
//!
//! ## Smart Widget Key Commands
//! ```ignore
//! | KEY | ACTION
//! |----------|-----------------------------------------------------------|
//! | h | Toggles target selector widget hidden/visible
//! | f | Toggle focus on the selected target only
//! | UP | Select previous target in target selector widget
//! | DOWN | Select next target in target selector widget
//! | LEFT | Reduce SHOWN (!) log messages by one level
//! | RIGHT | Increase SHOWN (!) log messages by one level
//! | - | Reduce CAPTURED (!) log messages by one level
//! | + | Increase CAPTURED (!) log messages by one level
//! | PAGEUP | Enter Page Mode and scroll approx. half page up in log history.
//! | PAGEDOWN | Only in page mode: scroll 10 events down in log history.
//! | ESCAPE | Exit page mode and go back to scrolling mode
//! | SPACE | Toggles hiding of targets, which have logfilter set to off
//! ```
//!
//! The mapping of key to action has to be done in the application. The respective TuiWidgetEvent
//! has to be provided to TuiWidgetState::transition().
//!
//! Remark to the page mode: The timestamp of the event at event history's bottom line is used as
//! reference. This means, changing the filters in the EWIDT/focus from the target selector window
//! should work as expected without jumps in the history. The page next/forward advances as
//! per visibility of the events.
//!
//! ## Basic usage to initialize logger-system:
//! ```rust
//! #[macro_use]
//! extern crate log;
//! //use tui_logger;
//!
//! fn main() {
//! // Early initialization of the logger
//!
//! // Set max_log_level to Trace
//! tui_logger::init_logger(log::LevelFilter::Trace).unwrap();
//!
//! // Set default level for unknown targets to Trace
//! tui_logger::set_default_level(log::LevelFilter::Trace);
//!
//! // code....
//! }
//! ```
//!
//! For use of the widget please check examples/demo.rs
//!
//! ## Demo
//!
//! Run demo using termion:
//!
//! ```ignore
//! cargo run --example demo --features termion
//! ```
//!
//! Run demo with crossterm:
//!
//! ```ignore
//! cargo run --example demo --features crossterm
//! ```
//!
//! Run demo using termion and simple custom formatter in bottom right log widget:
//!
//! ```ignore
//! cargo run --example demo --features termion,formatter
//! ```
//!
//! ## Configuration by environment variables
//!
//! `tui.logger` uses `env-filter` crate to support configuration by a string or an environment variable.
//! This is an opt-in by call to one of these two functions.
//! ```rust
//! pub fn set_env_filter_from_string(filterstring: &str) {}
//! pub fn set_env_filter_from_env(env_name: Option<&str>) {}
//! ```
//! Default environment variable name is `RUST_LOG`.
//!
//! ## `slog` support
//!
//! `tui-logger` provides a [`TuiSlogDrain`] which implements `slog::Drain` and will route all records
//! it receives to the `tui-logger` widget.
//!
//! Enabled by feature "slog-support"
//!
//! ## `tracing-subscriber` support
//!
//! `tui-logger` provides a [`TuiTracingSubscriberLayer`] which implements
//! `tracing_subscriber::Layer` and will collect all events
//! it receives to the `tui-logger` widget
//!
//! Enabled by feature "tracing-support"
//!
//! ## Custom filtering
//! ```rust
//! #[macro_use]
//! extern crate log;
//! //use tui_logger;
//! use env_logger;
//!
//! fn main() {
//! // Early initialization of the logger
//! let drain = tui_logger::Drain::new();
//! // instead of tui_logger::init_logger, we use `env_logger`
//! env_logger::Builder::default()
//! .format(move |buf, record|
//! // patch the env-logger entry through our drain to the tui-logger
//! Ok(drain.log(record))
//! ).init(); // make this the global logger
//! // code....
//! }
//! ```
//!
//! ## Custom formatting
//!
//! For experts only ! Configure along the lines:
//! ```ignore
//! use tui_logger::LogFormatter;
//!
//! let formatter = MyLogFormatter();
//!
//! TuiLoggerWidget::default()
//! .block(Block::bordered().title("Filtered TuiLoggerWidget"))
//! .formatter(formatter)
//! .state(&filter_state)
//! .render(left, buf);
//! ```
//! The example demo can be invoked to use a custom formatter as example for the bottom right widget.
//!
// Enable docsrs doc_cfg - to display non-default feature documentation.
#![cfg_attr(docsrs, feature(doc_cfg))]
#[macro_use]
extern crate lazy_static;
pub use env_filter;
mod circular;
pub use crate::circular::CircularBuffer;
#[cfg(feature = "slog-support")]
#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
mod slog;
#[cfg(feature = "slog-support")]
#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
pub use crate::slog::TuiSlogDrain;
#[cfg(feature = "tracing-support")]
#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
mod tracing_subscriber;
#[cfg(feature = "tracing-support")]
#[cfg_attr(docsrs, doc(cfg(feature = "tracing-support")))]
pub use crate::tracing_subscriber::TuiTracingSubscriberLayer;
#[doc(no_inline)]
pub use log::LevelFilter;
mod widget;
pub use widget::inner::TuiWidgetEvent;
pub use widget::inner::TuiWidgetState;
pub use widget::logformatter::LogFormatter;
pub use widget::smart::TuiLoggerSmartWidget;
pub use widget::standard::TuiLoggerWidget;
pub use widget::target::TuiLoggerTargetWidget;
mod config;
pub use config::LevelConfig;
mod file;
pub use file::TuiLoggerFile;
mod logger;
pub use crate::logger::api::*;
pub use crate::logger::ExtLogRecord;
pub use crate::logger::TuiLoggerLevelOutput;
use crate::logger::*;
================================================
FILE: src/logger/api.rs
================================================
use std::thread;
use crate::logger::fast_hash::fast_str_hash;
use crate::CircularBuffer;
use crate::TuiLoggerFile;
use log::LevelFilter;
use log::Record;
use log::SetLoggerError;
use crate::TUI_LOGGER;
// Lots of boilerplate code, so that init_logger can return two error types...
#[derive(Debug)]
pub enum TuiLoggerError {
SetLoggerError(SetLoggerError),
ThreadError(std::io::Error),
}
impl std::error::Error for TuiLoggerError {
fn description(&self) -> &str {
match self {
TuiLoggerError::SetLoggerError(_) => "SetLoggerError",
TuiLoggerError::ThreadError(_) => "ThreadError",
}
}
fn cause(&self) -> Option<&dyn std::error::Error> {
match self {
TuiLoggerError::SetLoggerError(_) => None,
TuiLoggerError::ThreadError(err) => Some(err),
}
}
}
impl std::fmt::Display for TuiLoggerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TuiLoggerError::SetLoggerError(err) => write!(f, "SetLoggerError({})", err),
TuiLoggerError::ThreadError(err) => write!(f, "ThreadError({})", err),
}
}
}
/// Init the logger.
pub fn init_logger(max_level: LevelFilter) -> Result<(), TuiLoggerError> {
let join_handle = thread::Builder::new()
.name("tui-logger::move_events".into())
.spawn(|| {
let duration = std::time::Duration::from_millis(10);
loop {
thread::park_timeout(duration);
TUI_LOGGER.move_events();
}
})
.map_err(TuiLoggerError::ThreadError)?;
TUI_LOGGER.hot_log.lock().mover_thread = Some(join_handle);
if cfg!(feature = "tracing-support") {
set_default_level(max_level);
Ok(())
} else {
log::set_max_level(max_level);
log::set_logger(&*TUI_LOGGER).map_err(TuiLoggerError::SetLoggerError)
}
}
/// Set the depth of the hot buffer in order to avoid message loss.
/// This is effective only after a call to move_events()
pub fn set_hot_buffer_depth(depth: usize) {
TUI_LOGGER.inner.lock().hot_depth = depth;
}
/// Set the depth of the circular buffer in order to avoid message loss.
/// This will delete all existing messages in the circular buffer.
pub fn set_buffer_depth(depth: usize) {
TUI_LOGGER.inner.lock().events = CircularBuffer::new(depth);
}
/// Define filename and log formmating options for file dumping.
pub fn set_log_file(file_options: TuiLoggerFile) {
TUI_LOGGER.inner.lock().dump = Some(file_options);
}
/// Set default levelfilter for unknown targets of the logger
pub fn set_default_level(levelfilter: LevelFilter) {
TUI_LOGGER.hot_select.lock().default = levelfilter;
TUI_LOGGER.inner.lock().default = levelfilter;
}
/// Remove env filter - for debugging purposes
pub fn remove_env_filter() {
TUI_LOGGER.hot_select.lock().filter = None;
TUI_LOGGER.inner.lock().filter = None;
}
fn set_env_filter(filter1: env_filter::Filter, filter2: env_filter::Filter) {
// Filter does not support Copy. In order to not unnecessary lock hot_select,
// we use a manual copy of the env filter.
TUI_LOGGER.hot_select.lock().filter = Some(filter1);
TUI_LOGGER.inner.lock().filter = Some(filter2);
}
/// Parse environment variable for env_filter
pub fn set_env_filter_from_string(filterstring: &str) {
let mut builder1 = env_filter::Builder::new();
let mut builder2 = env_filter::Builder::new();
builder1.parse(filterstring);
builder2.parse(filterstring);
set_env_filter(builder1.build(), builder2.build());
}
/// Parse environment variable for env_filter
pub fn set_env_filter_from_env(env_name: Option<&str>) {
let mut builder1 = env_filter::Builder::new();
let mut builder2 = env_filter::Builder::new();
// Parse a directives string from an environment variable
if let Ok(ref filter) = std::env::var(env_name.unwrap_or("RUST_LOG")) {
builder1.parse(filter);
builder2.parse(filter);
set_env_filter(builder1.build(), builder2.build());
}
}
/// Set levelfilter for a specific target in the logger
pub fn set_level_for_target(target: &str, levelfilter: LevelFilter) {
let h = fast_str_hash(target);
TUI_LOGGER.inner.lock().targets.set(target, levelfilter);
let mut hs = TUI_LOGGER.hot_select.lock();
hs.hashtable.insert(h, levelfilter);
}
// Move events from the hot log to the main log
pub fn move_events() {
TUI_LOGGER.move_events();
}
/// A simple `Drain` to log any event directly.
#[derive(Default)]
pub struct Drain;
impl Drain {
/// Create a new Drain
pub fn new() -> Self {
Drain
}
/// Log the given record to the main tui-logger
pub fn log(&self, record: &Record) {
TUI_LOGGER.raw_log(record)
}
}
================================================
FILE: src/logger/fast_hash.rs
================================================
/// A very simple and fast string hash function, based on the Java String.hashCode() algorithm.
pub fn fast_str_hash(s: &str) -> u64 {
let mut hash: u64 = 0xdeadbeef;
for b in s.bytes() {
hash = hash.wrapping_mul(31).wrapping_add(b as u64);
}
hash
}
================================================
FILE: src/logger/inner.rs
================================================
use crate::logger::fast_hash::fast_str_hash;
use crate::{CircularBuffer, LevelConfig, TuiLoggerFile};
use env_filter::Filter;
use jiff::Zoned;
use log::{Level, LevelFilter, Log, Metadata, Record};
use parking_lot::Mutex;
use std::collections::HashMap;
use std::io::Write;
use std::mem;
use std::thread;
/// The TuiLoggerWidget shows the logging messages in an endless scrolling view.
/// It is controlled by a TuiWidgetState for selected events.
#[derive(Debug, Clone, Copy, PartialEq, Hash)]
pub enum TuiLoggerLevelOutput {
Abbreviated,
Long,
}
/// These are the sub-structs for the static TUI_LOGGER struct.
pub(crate) struct HotSelect {
pub filter: Option<Filter>,
pub hashtable: HashMap<u64, LevelFilter>,
pub default: LevelFilter,
}
pub(crate) struct HotLog {
pub events: CircularBuffer<ExtLogRecord>,
pub mover_thread: Option<thread::JoinHandle<()>>,
}
enum StringOrStatic {
StaticString(&'static str),
IsString(String),
}
impl StringOrStatic {
fn as_str(&self) -> &str {
match self {
Self::StaticString(s) => s,
Self::IsString(s) => s,
}
}
}
pub struct ExtLogRecord {
pub timestamp: Zoned,
pub level: Level,
target: String,
file: Option<StringOrStatic>,
module_path: Option<StringOrStatic>,
pub line: Option<u32>,
msg: String,
}
impl ExtLogRecord {
#[inline]
pub fn target(&self) -> &str {
&self.target
}
#[inline]
pub fn file(&self) -> Option<&str> {
self.file.as_ref().map(|f| f.as_str())
}
#[inline]
pub fn module_path(&self) -> Option<&str> {
self.module_path.as_ref().map(|mp| mp.as_str())
}
#[inline]
pub fn msg(&self) -> &str {
&self.msg
}
fn from(record: &Record) -> Self {
let file: Option<StringOrStatic> = record
.file_static()
.map(StringOrStatic::StaticString)
.or_else(|| {
record
.file()
.map(|s| StringOrStatic::IsString(s.to_string()))
});
let module_path: Option<StringOrStatic> = record
.module_path_static()
.map(StringOrStatic::StaticString)
.or_else(|| {
record
.module_path()
.map(|s| StringOrStatic::IsString(s.to_string()))
});
ExtLogRecord {
timestamp: Zoned::now(),
level: record.level(),
target: record.target().to_string(),
file,
module_path,
line: record.line(),
msg: format!("{}", record.args()),
}
}
fn overrun(timestamp: Zoned, total: usize, elements: usize) -> Self {
ExtLogRecord {
timestamp,
level: Level::Warn,
target: "TuiLogger".to_string(),
file: None,
module_path: None,
line: None,
msg: format!(
"There have been {} events lost, {} recorded out of {}",
total - elements,
elements,
total
),
}
}
}
pub(crate) struct TuiLoggerInner {
pub hot_depth: usize,
pub events: CircularBuffer<ExtLogRecord>,
pub dump: Option<TuiLoggerFile>,
pub total_events: usize,
pub default: LevelFilter,
pub targets: LevelConfig,
pub filter: Option<Filter>,
}
pub struct TuiLogger {
pub hot_select: Mutex<HotSelect>,
pub hot_log: Mutex<HotLog>,
pub inner: Mutex<TuiLoggerInner>,
}
impl TuiLogger {
pub fn move_events(&self) {
// If there are no new events, then just return
if self.hot_log.lock().events.total_elements() == 0 {
return;
}
// Exchange new event buffer with the hot buffer
let mut received_events = {
let hot_depth = self.inner.lock().hot_depth;
let new_circular = CircularBuffer::new(hot_depth);
let mut hl = self.hot_log.lock();
mem::replace(&mut hl.events, new_circular)
};
let mut tli = self.inner.lock();
let total = received_events.total_elements();
let elements = received_events.len();
tli.total_events += total;
let mut consumed = received_events.take();
let mut reversed = Vec::with_capacity(consumed.len() + 1);
while let Some(log_entry) = consumed.pop() {
reversed.push(log_entry);
}
if total > elements {
// Too many events received, so some have been lost
let new_log_entry = ExtLogRecord::overrun(
reversed[reversed.len() - 1].timestamp.clone(),
total,
elements,
);
reversed.push(new_log_entry);
}
while let Some(log_entry) = reversed.pop() {
if tli.targets.get(&log_entry.target).is_none() {
let mut default_level = tli.default;
if let Some(filter) = tli.filter.as_ref() {
// Let's check, what the environment filter says about this target.
let metadata = log::MetadataBuilder::new()
.level(log_entry.level)
.target(&log_entry.target)
.build();
if filter.enabled(&metadata) {
// There is no direct access to the levelFilter, so we have to iterate over all possible level filters.
for lf in [
LevelFilter::Trace,
LevelFilter::Debug,
LevelFilter::Info,
LevelFilter::Warn,
LevelFilter::Error,
] {
let metadata = log::MetadataBuilder::new()
.level(lf.to_level().unwrap())
.target(&log_entry.target)
.build();
if filter.enabled(&metadata) {
// Found the related level filter
default_level = lf;
// In order to avoid checking the directives again,
// we store the level filter in the hashtable for the hot path
let h = fast_str_hash(&log_entry.target);
self.hot_select.lock().hashtable.insert(h, lf);
break;
}
}
}
}
tli.targets.set(&log_entry.target, default_level);
}
if let Some(ref mut file_options) = tli.dump {
let mut output = String::new();
let (lev_long, lev_abbr, with_loc) = match log_entry.level {
log::Level::Error => ("ERROR", "E", true),
log::Level::Warn => ("WARN ", "W", true),
log::Level::Info => ("INFO ", "I", false),
log::Level::Debug => ("DEBUG", "D", true),
log::Level::Trace => ("TRACE", "T", true),
};
if let Some(fmt) = file_options.timestamp_fmt.as_ref() {
output.push_str(&log_entry.timestamp.strftime(fmt).to_string());
output.push(file_options.format_separator);
}
match file_options.format_output_level {
None => {}
Some(TuiLoggerLevelOutput::Abbreviated) => {
output.push_str(lev_abbr);
output.push(file_options.format_separator);
}
Some(TuiLoggerLevelOutput::Long) => {
output.push_str(lev_long);
output.push(file_options.format_separator);
}
}
if file_options.format_output_target {
output.push_str(&log_entry.target);
output.push(file_options.format_separator);
}
if with_loc {
if file_options.format_output_file {
if let Some(file) = log_entry.file() {
output.push_str(file);
output.push(file_options.format_separator);
}
}
if file_options.format_output_line {
if let Some(line) = log_entry.line.as_ref() {
output.push_str(&format!("{}", line));
output.push(file_options.format_separator);
}
}
}
output.push_str(&log_entry.msg);
if let Err(_e) = writeln!(file_options.dump, "{}", output) {
// TODO: What to do in case of write error ?
}
}
tli.events.push(log_entry);
}
}
}
lazy_static! {
pub static ref TUI_LOGGER: TuiLogger = {
let hs = HotSelect {
filter: None,
hashtable: HashMap::with_capacity(1000),
default: LevelFilter::Info,
};
let hl = HotLog {
events: CircularBuffer::new(1000),
mover_thread: None,
};
let tli = TuiLoggerInner {
hot_depth: 1000,
events: CircularBuffer::new(10000),
total_events: 0,
dump: None,
default: LevelFilter::Info,
targets: LevelConfig::new(),
filter: None,
};
TuiLogger {
hot_select: Mutex::new(hs),
hot_log: Mutex::new(hl),
inner: Mutex::new(tli),
}
};
}
impl Log for TuiLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
let h = fast_str_hash(metadata.target());
let hs = self.hot_select.lock();
if let Some(&levelfilter) = hs.hashtable.get(&h) {
metadata.level() <= levelfilter
} else if let Some(envfilter) = hs.filter.as_ref() {
envfilter.enabled(metadata)
} else {
metadata.level() <= hs.default
}
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
self.raw_log(record)
}
}
fn flush(&self) {}
}
impl TuiLogger {
pub fn raw_log(&self, record: &Record) {
let log_entry = ExtLogRecord::from(record);
let mut events_lock = self.hot_log.lock();
events_lock.events.push(log_entry);
let need_signal = events_lock
.events
.total_elements()
.is_multiple_of(events_lock.events.capacity() / 2);
if need_signal {
if let Some(jh) = events_lock.mover_thread.as_ref() {
thread::Thread::unpark(jh.thread());
}
}
}
}
================================================
FILE: src/logger/mod.rs
================================================
pub mod api;
mod fast_hash;
mod inner;
pub use inner::*;
================================================
FILE: src/slog.rs
================================================
//! `slog` support for `tui-logger`
use super::TUI_LOGGER;
use log::{self, Log, Record};
use slog::{self, Drain, KV};
use std::{fmt, io};
#[cfg_attr(docsrs, doc(cfg(feature = "slog-support")))]
pub fn slog_drain() -> TuiSlogDrain {
TuiSlogDrain
}
/// Key-Separator-Value serializer
// Copied from `slog-stdlog`
struct Ksv<W: io::Write> {
io: W,
}
impl<W: io::Write> Ksv<W> {
fn new(io: W) -> Self {
Ksv { io }
}
fn into_inner(self) -> W {
self.io
}
}
impl<W: io::Write> slog::Serializer for Ksv<W> {
fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result {
write!(self.io, ", {}: {}", key, val)?;
Ok(())
}
}
// Copied from `slog-stdlog`
struct LazyLogString<'a> {
info: &'a slog::Record<'a>,
logger_values: &'a slog::OwnedKVList,
}
impl<'a> LazyLogString<'a> {
fn new(info: &'a slog::Record, logger_values: &'a slog::OwnedKVList) -> Self {
LazyLogString {
info,
logger_values,
}
}
}
impl<'a> fmt::Display for LazyLogString<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.info.msg())?;
let io = io::Cursor::new(Vec::new());
let mut ser = Ksv::new(io);
self.logger_values
.serialize(self.info, &mut ser)
.map_err(|_| fmt::Error)?;
self.info
.kv()
.serialize(self.info, &mut ser)
.map_err(|_| fmt::Error)?;
let values = ser.into_inner().into_inner();
write!(f, "{}", String::from_utf8_lossy(&values))
}
}
#[allow(clippy::needless_doctest_main)]
/// slog-compatible Drain that feeds messages to `tui-logger`.
///
/// ## Basic usage:
/// ```
/// use slog::{self, o, Drain, info};
/// //use tui_logger;
///
/// fn main() {
/// let drain = tui_logger::slog_drain().fuse();
/// let log = slog::Logger::root(drain, o!());
/// info!(log, "Logging via slog works!");
///
/// }
pub struct TuiSlogDrain;
impl Drain for TuiSlogDrain {
type Ok = ();
type Err = io::Error;
// Copied from `slog-stdlog`
fn log(&self, info: &slog::Record, logger_values: &slog::OwnedKVList) -> io::Result<()> {
let level = match info.level() {
slog::Level::Critical | slog::Level::Error => log::Level::Error,
slog::Level::Warning => log::Level::Warn,
slog::Level::Info => log::Level::Info,
slog::Level::Debug => log::Level::Debug,
slog::Level::Trace => log::Level::Trace,
};
let mut target = info.tag();
if target.is_empty() {
target = info.module();
}
let lazy = LazyLogString::new(info, logger_values);
TUI_LOGGER.log(
&Record::builder()
.args(format_args!("{}", lazy))
.level(level)
.target(target)
.file(Some(info.file()))
.line(Some(info.line()))
.build(),
);
Ok(())
}
}
================================================
FILE: src/tracing_subscriber.rs
================================================
//! `tracing-subscriber` support for `tui-logger`
use super::TUI_LOGGER;
use log::{self, Log, Record};
use std::collections::BTreeMap;
use std::fmt;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
#[derive(Default)]
struct ToStringVisitor<'a>(BTreeMap<&'a str, String>);
impl fmt::Display for ToStringVisitor<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.iter().try_for_each(|(k, v)| -> fmt::Result {
if *k == "message" {
write!(f, " {}", v)
} else {
write!(f, " {}: {}", k, v)
}
})
}
}
impl<'a> tracing::field::Visit for ToStringVisitor<'a> {
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_error(
&mut self,
field: &tracing::field::Field,
value: &(dyn std::error::Error + 'static),
) {
self.0
.insert(field.name(), format_args!("{}", value).to_string());
}
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
self.0
.insert(field.name(), format_args!("{:?}", value).to_string());
}
}
#[allow(clippy::needless_doctest_main)]
/// tracing-subscriber-compatible layer that feeds messages to `tui-logger`.
///
/// ## How it works
/// Under the hood, tui_logger still uses `log`. `tracing` events are mapped to
/// `log` events internally (which are then fed to `tui-logger`).
///
/// ## Usage note
/// As per the example below, [init_logger()] must be called prior to logging events.
///
/// [init_logger()]: crate::init_logger()
/// ## Basic usage
/// ```
/// use tracing_subscriber::prelude::*;
///
/// fn main() {
/// tracing_subscriber::registry()
/// .with(tui_logger::TuiTracingSubscriberLayer)
/// .init();
/// tui_logger::init_logger(tui_logger::LevelFilter::Trace).unwrap();
/// tracing::info!("Logging via tracing works!");
/// }
/// ```
struct SpanAttributes {
attributes: String,
}
pub struct TuiTracingSubscriberLayer;
impl<S> Layer<S> for TuiTracingSubscriberLayer
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(
&self,
attrs: &tracing::span::Attributes<'_>,
id: &tracing::span::Id,
ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let mut visitor = ToStringVisitor::default();
attrs.record(&mut visitor);
ctx.span(id)
.unwrap()
.extensions_mut()
.insert(SpanAttributes {
attributes: format!("{}", visitor),
});
}
fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = ToStringVisitor::default();
event.record(&mut visitor);
let span_attributes = ctx
.event_span(event)
.and_then(|s| {
s.extensions()
.get::<SpanAttributes>()
.map(|a| a.attributes.to_owned())
})
.unwrap_or_else(String::new);
let level = match *event.metadata().level() {
tracing::Level::ERROR => log::Level::Error,
tracing::Level::WARN => log::Level::Warn,
tracing::Level::INFO => log::Level::Info,
tracing::Level::DEBUG => log::Level::Debug,
tracing::Level::TRACE => log::Level::Trace,
};
TUI_LOGGER.log(
&Record::builder()
.args(format_args!("{}{}", span_attributes, visitor))
.level(level)
.target(event.metadata().target())
.file(event.metadata().file())
.line(event.metadata().line())
.module_path(event.metadata().module_path())
.build(),
);
}
}
================================================
FILE: src/widget/inner.rs
================================================
use std::sync::Arc;
use log::LevelFilter;
use parking_lot::Mutex;
use crate::{set_level_for_target, LevelConfig};
#[derive(Clone, Copy, Debug)]
pub(crate) struct LinePointer {
pub event_index: usize, // into event buffer
pub subline: usize,
}
/// This struct contains the shared state of a TuiLoggerWidget and a TuiLoggerTargetWidget.
#[derive(Default)]
pub struct TuiWidgetState {
inner: Arc<Mutex<TuiWidgetInnerState>>,
}
impl TuiWidgetState {
/// Create a new TuiWidgetState
pub fn new() -> TuiWidgetState {
TuiWidgetState {
inner: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
}
}
pub fn set_default_display_level(self, levelfilter: LevelFilter) -> TuiWidgetState {
self.inner
.lock()
.config
.set_default_display_level(levelfilter);
self
}
pub fn set_level_for_target(self, target: &str, levelfilter: LevelFilter) -> TuiWidgetState {
self.inner.lock().config.set(target, levelfilter);
self
}
pub fn transition(&self, event: TuiWidgetEvent) {
self.inner.lock().transition(event);
}
pub fn clone_state(&self) -> Arc<Mutex<TuiWidgetInnerState>> {
self.inner.clone()
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum TuiWidgetEvent {
SpaceKey,
UpKey,
DownKey,
LeftKey,
RightKey,
PlusKey,
MinusKey,
HideKey,
FocusKey,
PrevPageKey,
NextPageKey,
EscapeKey,
}
#[derive(Default)]
pub struct TuiWidgetInnerState {
pub(crate) config: LevelConfig,
pub(crate) nr_items: usize,
pub(crate) selected: usize,
pub(crate) opt_line_pointer_center: Option<LinePointer>,
pub(crate) opt_line_pointer_next_page: Option<LinePointer>,
pub(crate) opt_line_pointer_prev_page: Option<LinePointer>,
pub(crate) opt_selected_target: Option<String>,
pub(crate) opt_selected_visibility_more: Option<LevelFilter>,
pub(crate) opt_selected_visibility_less: Option<LevelFilter>,
pub(crate) opt_selected_recording_more: Option<LevelFilter>,
pub(crate) opt_selected_recording_less: Option<LevelFilter>,
pub(crate) offset: usize,
pub(crate) hide_off: bool,
pub(crate) hide_target: bool,
pub(crate) focus_selected: bool,
}
impl TuiWidgetInnerState {
pub fn new() -> TuiWidgetInnerState {
TuiWidgetInnerState::default()
}
fn transition(&mut self, event: TuiWidgetEvent) {
use TuiWidgetEvent::*;
match event {
SpaceKey => {
self.hide_off ^= true;
}
HideKey => {
self.hide_target ^= true;
}
FocusKey => {
self.focus_selected ^= true;
}
UpKey => {
if !self.hide_target && self.selected > 0 {
self.selected -= 1;
}
}
DownKey => {
if !self.hide_target && self.selected + 1 < self.nr_items {
self.selected += 1;
}
}
LeftKey => {
if let Some(selected_target) = self.opt_selected_target.take() {
if let Some(selected_visibility_less) = self.opt_selected_visibility_less.take()
{
self.config.set(&selected_target, selected_visibility_less);
}
}
}
RightKey => {
if let Some(selected_target) = self.opt_selected_target.take() {
if let Some(selected_visibility_more) = self.opt_selected_visibility_more.take()
{
self.config.set(&selected_target, selected_visibility_more);
}
}
}
PlusKey => {
if let Some(selected_target) = self.opt_selected_target.take() {
if let Some(selected_recording_more) = self.opt_selected_recording_more.take() {
set_level_for_target(&selected_target, selected_recording_more);
}
}
}
MinusKey => {
if let Some(selected_target) = self.opt_selected_target.take() {
if let Some(selected_recording_less) = self.opt_selected_recording_less.take() {
set_level_for_target(&selected_target, selected_recording_less);
}
}
}
PrevPageKey => self.opt_line_pointer_center = self.opt_line_pointer_prev_page,
NextPageKey => self.opt_line_pointer_center = self.opt_line_pointer_next_page,
EscapeKey => self.opt_line_pointer_center = None,
}
}
}
================================================
FILE: src/widget/logformatter.rs
================================================
use crate::ExtLogRecord;
use ratatui::text::Line;
pub trait LogFormatter: Send + Sync {
fn min_width(&self) -> u16;
/// This must format any event in one or more lines.
/// Correct wrapping in next line with/without indenting must be performed here.
/// The parameter width is the available line width
fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>>;
}
================================================
FILE: src/widget/mod.rs
================================================
pub mod inner;
pub mod logformatter;
pub mod smart;
pub mod standard;
mod standard_formatter;
pub mod target;
================================================
FILE: src/widget/smart.rs
================================================
use crate::widget::logformatter::LogFormatter;
use parking_lot::Mutex;
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;
use log::LevelFilter;
use ratatui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::Style,
text::Line,
widgets::{Block, BorderType, Borders, Widget},
};
use crate::logger::TuiLoggerLevelOutput;
use crate::logger::TUI_LOGGER;
use crate::{TuiLoggerTargetWidget, TuiWidgetState};
use super::{inner::TuiWidgetInnerState, standard::TuiLoggerWidget};
/// The Smart Widget combines the TuiLoggerWidget and the TuiLoggerTargetWidget
/// into a nice combo, where the TuiLoggerTargetWidget can be shown/hidden.
///
/// In the title the number of logging messages/s in the whole buffer is shown.
pub struct TuiLoggerSmartWidget<'a> {
title_log: Line<'a>,
title_target: Line<'a>,
style: Option<Style>,
border_style: Style,
border_type: BorderType,
highlight_style: Option<Style>,
logformatter: Option<Box<dyn LogFormatter>>,
style_error: Option<Style>,
style_warn: Option<Style>,
style_debug: Option<Style>,
style_trace: Option<Style>,
style_info: Option<Style>,
style_show: Option<Style>,
style_hide: Option<Style>,
style_off: Option<Style>,
format_separator: Option<char>,
format_timestamp: Option<Option<String>>,
format_output_level: Option<Option<TuiLoggerLevelOutput>>,
format_output_target: Option<bool>,
format_output_file: Option<bool>,
format_output_line: Option<bool>,
state: Arc<Mutex<TuiWidgetInnerState>>,
}
impl<'a> Default for TuiLoggerSmartWidget<'a> {
fn default() -> Self {
TuiLoggerSmartWidget {
title_log: Line::from("Tui Log"),
title_target: Line::from("Tui Target Selector"),
style: None,
border_style: Style::default(),
border_type: BorderType::Plain,
highlight_style: None,
logformatter: None,
style_error: None,
style_warn: None,
style_debug: None,
style_trace: None,
style_info: None,
style_show: None,
style_hide: None,
style_off: None,
format_separator: None,
format_timestamp: None,
format_output_level: None,
format_output_target: None,
format_output_file: None,
format_output_line: None,
state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
}
}
}
impl<'a> TuiLoggerSmartWidget<'a> {
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = Some(style);
self
}
pub fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
pub fn border_type(mut self, border_type: BorderType) -> Self {
self.border_type = border_type;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
pub fn style_error(mut self, style: Style) -> Self {
self.style_error = Some(style);
self
}
pub fn style_warn(mut self, style: Style) -> Self {
self.style_warn = Some(style);
self
}
pub fn style_info(mut self, style: Style) -> Self {
self.style_info = Some(style);
self
}
pub fn style_trace(mut self, style: Style) -> Self {
self.style_trace = Some(style);
self
}
pub fn style_debug(mut self, style: Style) -> Self {
self.style_debug = Some(style);
self
}
pub fn style_off(mut self, style: Style) -> Self {
self.style_off = Some(style);
self
}
pub fn style_hide(mut self, style: Style) -> Self {
self.style_hide = Some(style);
self
}
pub fn style_show(mut self, style: Style) -> Self {
self.style_show = Some(style);
self
}
/// Separator character between field.
/// Default is ':'
pub fn output_separator(mut self, sep: char) -> Self {
self.format_separator = Some(sep);
self
}
/// The format string can be defined as described in
/// <https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html>
///
/// If called with None, timestamp is not included in output.
///
/// Default is %H:%M:%S
pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
self.format_timestamp = Some(fmt);
self
}
/// Possible values are
/// - TuiLoggerLevelOutput::Long => DEBUG/TRACE/...
/// - TuiLoggerLevelOutput::Abbreviated => D/T/...
///
/// If called with None, level is not included in output.
///
/// Default is Long
pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
self.format_output_level = Some(level);
self
}
/// Enables output of target field of event
///
/// Default is true
pub fn output_target(mut self, enabled: bool) -> Self {
self.format_output_target = Some(enabled);
self
}
/// Enables output of file field of event
///
/// Default is true
pub fn output_file(mut self, enabled: bool) -> Self {
self.format_output_file = Some(enabled);
self
}
/// Enables output of line field of event
///
/// Default is true
pub fn output_line(mut self, enabled: bool) -> Self {
self.format_output_line = Some(enabled);
self
}
pub fn title_target<T>(mut self, title: T) -> Self
where
T: Into<Line<'a>>,
{
self.title_target = title.into();
self
}
pub fn title_log<T>(mut self, title: T) -> Self
where
T: Into<Line<'a>>,
{
self.title_log = title.into();
self
}
pub fn state(mut self, state: &TuiWidgetState) -> Self {
self.state = state.clone_state();
self
}
}
impl<'a> Widget for TuiLoggerSmartWidget<'a> {
/// Nothing to draw for combo widget
fn render(self, area: Rect, buf: &mut Buffer) {
let entries_s = {
let mut tui_lock = TUI_LOGGER.inner.lock();
let first_timestamp = tui_lock
.events
.iter()
.next()
.map(|entry| entry.timestamp.timestamp().as_millisecond());
let last_timestamp = tui_lock
.events
.rev_iter()
.next()
.map(|entry| entry.timestamp.timestamp().as_millisecond());
if let Some(first) = first_timestamp {
if let Some(last) = last_timestamp {
let dt = last - first;
if dt > 0 {
tui_lock.events.len() as f64 / (dt as f64) * 1000.0
} else {
0.0
}
} else {
0.0
}
} else {
0.0
}
};
let mut title_log = self.title_log.clone();
title_log
.spans
.push(format!(" [log={:.1}/s]", entries_s).into());
let hide_target = self.state.lock().hide_target;
if hide_target {
let tui_lw = TuiLoggerWidget::default()
.block(
Block::default()
.title(title_log)
.border_style(self.border_style)
.border_type(self.border_type)
.borders(Borders::ALL),
)
.opt_style(self.style)
.opt_style_error(self.style_error)
.opt_style_warn(self.style_warn)
.opt_style_info(self.style_info)
.opt_style_debug(self.style_debug)
.opt_style_trace(self.style_trace)
.opt_output_separator(self.format_separator)
.opt_output_timestamp(self.format_timestamp)
.opt_output_level(self.format_output_level)
.opt_output_target(self.format_output_target)
.opt_output_file(self.format_output_file)
.opt_output_line(self.format_output_line)
.inner_state(self.state);
tui_lw.render(area, buf);
} else {
let mut width: usize = 0;
{
let hot_targets = &TUI_LOGGER.inner.lock().targets;
let mut state = self.state.lock();
let hide_off = state.hide_off;
{
let targets = &mut state.config;
targets.merge(hot_targets);
for (t, levelfilter) in targets.iter() {
if hide_off && levelfilter == &LevelFilter::Off {
continue;
}
width = width.max(t.graphemes(true).count())
}
}
}
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![
Constraint::Length(width as u16 + 6 + 2),
Constraint::Min(10),
])
.split(area);
let tui_ltw = TuiLoggerTargetWidget::default()
.block(
Block::default()
.title(self.title_target)
.border_style(self.border_style)
.border_type(self.border_type)
.borders(Borders::ALL),
)
.opt_style(self.style)
.opt_highlight_style(self.highlight_style)
.opt_style_off(self.style_off)
.opt_style_hide(self.style_hide)
.opt_style_show(self.style_show)
.inner_state(self.state.clone());
tui_ltw.render(chunks[0], buf);
let tui_lw = TuiLoggerWidget::default()
.block(
Block::default()
.title(title_log)
.border_style(self.border_style)
.border_type(self.border_type)
.borders(Borders::ALL),
)
.opt_formatter(self.logformatter)
.opt_style(self.style)
.opt_style_error(self.style_error)
.opt_style_warn(self.style_warn)
.opt_style_info(self.style_info)
.opt_style_debug(self.style_debug)
.opt_style_trace(self.style_trace)
.opt_output_separator(self.format_separator)
.opt_output_timestamp(self.format_timestamp)
.opt_output_level(self.format_output_level)
.opt_output_target(self.format_output_target)
.opt_output_file(self.format_output_file)
.opt_output_line(self.format_output_line)
.inner_state(self.state.clone());
tui_lw.render(chunks[1], buf);
}
}
}
================================================
FILE: src/widget/standard.rs
================================================
use crate::widget::logformatter::LogFormatter;
use crate::widget::standard_formatter::LogStandardFormatter;
use parking_lot::Mutex;
use std::sync::Arc;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
text::Line,
widgets::{Block, Widget},
};
use crate::widget::inner::LinePointer;
use crate::{CircularBuffer, ExtLogRecord, TuiLoggerLevelOutput, TuiWidgetState, TUI_LOGGER};
use super::inner::TuiWidgetInnerState;
pub struct TuiLoggerWidget<'b> {
block: Option<Block<'b>>,
logformatter: Option<Box<dyn LogFormatter>>,
/// Base style of the widget
style: Style,
/// Level based style
style_error: Option<Style>,
style_warn: Option<Style>,
style_debug: Option<Style>,
style_trace: Option<Style>,
style_info: Option<Style>,
format_separator: char,
format_timestamp: Option<String>,
format_output_level: Option<TuiLoggerLevelOutput>,
format_output_target: bool,
format_output_file: bool,
format_output_line: bool,
state: Arc<Mutex<TuiWidgetInnerState>>,
}
impl<'b> Default for TuiLoggerWidget<'b> {
fn default() -> TuiLoggerWidget<'b> {
TuiLoggerWidget {
block: None,
logformatter: None,
style: Default::default(),
style_error: None,
style_warn: None,
style_debug: None,
style_trace: None,
style_info: None,
format_separator: ':',
format_timestamp: Some("%H:%M:%S".to_string()),
format_output_level: Some(TuiLoggerLevelOutput::Long),
format_output_target: true,
format_output_file: true,
format_output_line: true,
state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
}
}
}
impl<'b> TuiLoggerWidget<'b> {
pub fn block(mut self, block: Block<'b>) -> Self {
self.block = Some(block);
self
}
pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>) -> Self {
self.logformatter = formatter;
self
}
pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
self.logformatter = Some(formatter);
self
}
pub fn opt_style(mut self, style: Option<Style>) -> Self {
if let Some(s) = style {
self.style = s;
}
self
}
pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_error = style;
}
self
}
pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_warn = style;
}
self
}
pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_info = style;
}
self
}
pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_trace = style;
}
self
}
pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
if style.is_some() {
self.style_debug = style;
}
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn style_error(mut self, style: Style) -> Self {
self.style_error = Some(style);
self
}
pub fn style_warn(mut self, style: Style) -> Self {
self.style_warn = Some(style);
self
}
pub fn style_info(mut self, style: Style) -> Self {
self.style_info = Some(style);
self
}
pub fn style_trace(mut self, style: Style) -> Self {
self.style_trace = Some(style);
self
}
pub fn style_debug(mut self, style: Style) -> Self {
self.style_debug = Some(style);
self
}
pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
if let Some(ch) = opt_sep {
self.format_separator = ch;
}
self
}
/// Separator character between field.
/// Default is ':'
pub fn output_separator(mut self, sep: char) -> Self {
self.format_separator = sep;
self
}
pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -> Self {
if let Some(fmt) = opt_fmt {
self.format_timestamp = fmt;
}
self
}
/// The format string can be defined as described in
/// <https://docs.rs/jiff/latest/jiff/fmt/strtime/index.html>
///
/// If called with None, timestamp is not included in output.
///
/// Default is %H:%M:%S
pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
self.format_timestamp = fmt;
self
}
pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelOutput>>) -> Self {
if let Some(fmt) = opt_fmt {
self.format_output_level = fmt;
}
self
}
/// Possible values are
/// - TuiLoggerLevelOutput::Long => DEBUG/TRACE/...
/// - TuiLoggerLevelOutput::Abbreviated => D/T/...
///
/// If called with None, level is not included in output.
///
/// Default is Long
pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Self {
self.format_output_level = level;
self
}
pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_target = enabled;
}
self
}
/// Enables output of target field of event
///
/// Default is true
pub fn output_target(mut self, enabled: bool) -> Self {
self.format_output_target = enabled;
self
}
pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_file = enabled;
}
self
}
/// Enables output of file field of event
///
/// Default is true
pub fn output_file(mut self, enabled: bool) -> Self {
self.format_output_file = enabled;
self
}
pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
if let Some(enabled) = opt_enabled {
self.format_output_line = enabled;
}
self
}
/// Enables output of line field of event
///
/// Default is true
pub fn output_line(mut self, enabled: bool) -> Self {
self.format_output_line = enabled;
self
}
pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> Self {
self.state = state;
self
}
pub fn state(mut self, state: &TuiWidgetState) -> Self {
self.state = state.clone_state();
self
}
fn next_event<'a>(
&self,
events: &'a CircularBuffer<ExtLogRecord>,
mut index: usize,
ignore_current: bool,
increment: bool,
state: &TuiWidgetInnerState,
) -> Option<(Option<usize>, usize, &'a ExtLogRecord)> {
// The result is an optional next_index, the event index and the event
if ignore_current {
index = if increment {
index + 1
} else {
if index == 0 {
return None;
}
index - 1
};
}
while let Some(evt) = events.element_at_index(index) {
let mut skip = false;
if let Some(level) = state
.config
.get(evt.target())
.or(state.config.get_default_display_level())
{
if level < evt.level {
skip = true;
}
}
if !skip && state.focus_selected {
if let Some(target) = state.opt_selected_target.as_ref() {
if target != evt.target() {
skip = true;
}
}
}
if skip {
index = if increment {
index + 1
} else {
if index == 0 {
break;
}
index - 1
};
} else if increment {
return Some((Some(index + 1), index, evt));
} else {
if index == 0 {
return Some((None, index, evt));
}
return Some((Some(index - 1), index, evt));
}
}
None
}
}
impl<'b> Widget for TuiLoggerWidget<'b> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let render_debug = false;
let formatter = match self.logformatter.take() {
Some(fmt) => fmt,
None => {
let fmt = LogStandardFormatter {
style: self.style,
style_error: self.style_error,
style_warn: self.style_warn,
style_debug: self.style_debug,
style_trace: self.style_trace,
style_info: self.style_info,
format_separator: self.format_separator,
format_timestamp: self.format_timestamp.clone(),
format_output_level: self.format_output_level,
format_output_target: self.format_output_target,
format_output_file: self.format_output_file,
format_output_line: self.format_output_line,
};
Box::new(fmt)
}
};
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if list_area.width < formatter.min_width() || list_area.height < 1 {
return;
}
let mut state = self.state.lock();
let la_height = list_area.height as usize;
let la_left = list_area.left();
let la_top = list_area.top();
let la_width = list_area.width as usize;
let mut rev_lines: Vec<(LinePointer, Line)> = vec![];
let mut can_scroll_up = true;
let mut can_scroll_down = state.opt_line_pointer_center.is_some();
{
enum Pos {
Top,
Bottom,
Center(usize),
}
let tui_lock = TUI_LOGGER.inner.lock();
// If scrolling, the opt_line_pointer_center is set.
// Otherwise we are following the bottom of the events
let opt_pos_event_index = if let Some(lp) = state.opt_line_pointer_center {
tui_lock.events.first_index().map(|first_index| {
if first_index <= lp.event_index {
(Pos::Center(lp.subline), lp.event_index)
} else {
(Pos::Top, first_index)
}
})
} else {
tui_lock
.events
.last_index()
.map(|last_index| (Pos::Bottom, last_index))
};
if let Some((pos, event_index)) = opt_pos_event_index {
// There are events to be shown
let mut lines: Vec<(usize, Vec<Line>, usize)> = Vec::new();
let mut from_line: isize = 0;
let mut to_line = 0;
match pos {
Pos::Center(subline) => {
if render_debug {
println!("CENTER {}", event_index);
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, true, &state)
{
let evt_lines = formatter.format(la_width, evt);
from_line = (la_height / 2) as isize - subline as isize;
to_line = la_height / 2 + (evt_lines.len() - 1) - subline;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Center is {}", evt_index);
}
}
}
Pos::Top => {
can_scroll_up = false;
if render_debug {
println!("TOP");
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, true, &state)
{
let evt_lines = formatter.format(la_width, evt);
from_line = 0;
to_line = evt_lines.len() - 1;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Top is {}", evt_index);
}
}
}
Pos::Bottom => {
if render_debug {
println!("TOP");
}
if let Some((_, evt_index, evt)) =
self.next_event(&tui_lock.events, event_index, false, false, &state)
{
let evt_lines = formatter.format(la_width, evt);
to_line = la_height - 1;
from_line = to_line as isize - (evt_lines.len() - 1) as isize;
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
if render_debug {
println!("Bottom is {}", evt_index);
}
}
}
}
if !lines.is_empty() {
let mut cont = true;
let mut at_top = false;
let mut at_bottom = false;
while cont {
if render_debug {
println!("from_line {}, to_line {}", from_line, to_line);
}
cont = false;
if from_line > 0 {
if let Some((_, evt_index, evt)) = self.next_event(
&tui_lock.events,
lines.first().as_ref().unwrap().0,
true,
false,
&state,
) {
let evt_lines = formatter.format(la_width, evt);
from_line -= evt_lines.len() as isize;
let n = evt_lines.len();
lines.insert(0, (evt_index, evt_lines, n));
cont = true;
} else {
// no more events, so adjust start
at_top = true;
if render_debug {
println!("no more events adjust start");
}
to_line -= from_line as usize;
from_line = 0;
if render_debug {
println!("=> from_line {}, to_line {}", from_line, to_line);
}
}
}
if to_line < la_height - 1 {
if let Some((_, evt_index, evt)) = self.next_event(
&tui_lock.events,
lines.last().as_ref().unwrap().0,
true,
true,
&state,
) {
let evt_lines = formatter.format(la_width, evt);
to_line += evt_lines.len();
let n = evt_lines.len();
lines.push((evt_index, evt_lines, n));
cont = true;
} else {
at_bottom = true;
can_scroll_down = false;
if render_debug {
println!("no more events at end");
}
// no more events
if to_line != la_height - 1 {
cont = true;
} else if !cont {
break;
}
// no more events, so adjust end
from_line += (la_height - 1 - to_line) as isize;
to_line = la_height - 1;
if render_debug {
println!("=> from_line {}, to_line {}", from_line, to_line);
}
}
}
if at_top && at_bottom {
break;
}
}
if at_top {
can_scroll_up = false;
}
if at_bottom {
can_scroll_down = false;
}
if render_debug {
println!("finished: from_line {}, to_line {}", from_line, to_line);
}
let mut curr: isize = to_line as isize;
while let Some((evt_index, evt_lines, mut n)) = lines.pop() {
for line in evt_lines.into_iter().rev() {
n -= 1;
if curr < 0 {
break;
}
if curr < la_height as isize {
let line_ptr = LinePointer {
event_index: evt_index,
subline: n,
};
rev_lines.push((line_ptr, line));
}
curr -= 1;
}
}
}
} else {
can_scroll_down = false;
can_scroll_up = false;
}
}
state.opt_line_pointer_next_page = if can_scroll_down {
rev_lines.first().map(|l| l.0)
} else {
None
};
state.opt_line_pointer_prev_page = if can_scroll_up {
rev_lines.last().map(|l| l.0)
} else {
None
};
if render_debug {
println!("Line pointers in buffer:");
for l in rev_lines.iter().rev() {
println!("event_index {}, subline {}", l.0.event_index, l.0.subline);
}
if state.opt_line_pointer_center.is_some() {
println!(
"Linepointer center: {:?}",
state.opt_line_pointer_center.unwrap()
);
}
if state.opt_line_pointer_next_page.is_some() {
println!(
"Linepointer next: {:?}",
state.opt_line_pointer_next_page.unwrap()
);
}
if state.opt_line_pointer_prev_page.is_some() {
println!(
"Linepointer prev: {:?}",
state.opt_line_pointer_prev_page.unwrap()
);
}
}
// This apparently ensures, that the log starts at top
let offset: u16 = if state.opt_line_pointer_center.is_none() {
0
} else {
let lines_cnt = rev_lines.len();
std::cmp::max(0, la_height - lines_cnt) as u16
};
for (i, line) in rev_lines.into_iter().rev().take(la_height).enumerate() {
line.1.render(
Rect {
x: la_left,
y: la_top + i as u16 + offset,
width: list_area.width,
height: 1,
},
buf,
)
}
}
}
================================================
FILE: src/widget/standard_formatter.rs
================================================
use crate::logger::TuiLoggerLevelOutput;
use crate::widget::logformatter::LogFormatter;
use crate::ExtLogRecord;
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
pub struct LogStandardFormatter {
/// Base style of the widget
pub(crate) style: Style,
/// Level based style
pub(crate) style_error: Option<Style>,
pub(crate) style_warn: Option<Style>,
pub(crate) style_debug: Option<Style>,
pub(crate) style_trace: Option<Style>,
pub(crate) style_info: Option<Style>,
pub(crate) format_separator: char,
pub(crate) format_timestamp: Option<String>,
pub(crate) format_output_level: Option<TuiLoggerLevelOutput>,
pub(crate) format_output_target: bool,
pub(crate) format_output_file: bool,
pub(crate) format_output_line: bool,
}
impl LogStandardFormatter {
fn append_wrapped_line(
&self,
style: Style,
indent: usize,
lines: &mut Vec<Line>,
line: &str,
width: usize,
with_indent: bool,
) {
let mut p = 0;
let mut wrap_len = width;
if with_indent {
wrap_len -= indent;
}
let space = " ".repeat(indent);
let line_chars = line.graphemes(true).collect::<Vec<_>>();
while p < line_chars.len() {
let linelen = std::cmp::min(wrap_len, line_chars.len() - p);
let subline = &line_chars[p..p + linelen];
let mut spans: Vec<Span> = Vec::new();
if wrap_len < width {
// need indent
spans.push(Span {
style,
content: Cow::Owned(space.to_string()),
});
}
spans.push(Span {
style,
content: Cow::Owned(subline.iter().map(|x| x.to_string()).collect()),
});
let line = Line::from(spans);
lines.push(line);
p += linelen;
// following lines need to be indented
wrap_len = width - indent;
}
}
}
impl LogFormatter for LogStandardFormatter {
fn min_width(&self) -> u16 {
9 + 4
}
fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let mut output = String::new();
let (col_style, lev_long, lev_abbr, with_loc) = match evt.level {
log::Level::Error => (self.style_error, "ERROR", "E", true),
log::Level::Warn => (self.style_warn, "WARN ", "W", true),
log::Level::Info => (self.style_info, "INFO ", "I", true),
log::Level::Debug => (self.style_debug, "DEBUG", "D", true),
log::Level::Trace => (self.style_trace, "TRACE", "T", true),
};
let col_style = col_style.unwrap_or(self.style);
if let Some(fmt) = self.format_timestamp.as_ref() {
output.push_str(&evt.timestamp.strftime(fmt).to_string());
output.push(self.format_separator);
}
match &self.format_output_level {
None => {}
Some(TuiLoggerLevelOutput::Abbreviated) => {
output.push_str(lev_abbr);
output.push(self.format_separator);
}
Some(TuiLoggerLevelOutput::Long) => {
output.push_str(lev_long);
output.push(self.format_separator);
}
}
if self.format_output_target {
output.push_str(evt.target());
output.push(self.format_separator);
}
if with_loc {
if self.format_output_file {
if let Some(file) = evt.file() {
output.push_str(file);
output.push(self.format_separator);
}
}
if self.format_output_line {
if let Some(line) = evt.line {
output.push_str(&format!("{}", line));
output.push(self.format_separator);
}
}
}
let mut sublines: Vec<&str> = evt.msg().lines().rev().collect();
output.push_str(sublines.pop().unwrap_or(""));
self.append_wrapped_line(col_style, 9, &mut lines, &output, width, false);
for subline in sublines.iter().rev() {
self.append_wrapped_line(col_style, 9, &mut lines, subline, width, true);
}
lines
}
}
================================================
FILE: src/widget/target.rs
================================================
use std::sync::Arc;
use parking_lot::Mutex;
use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Modifier, Style},
widgets::{Block, Widget},
};
use crate::logger::TUI_LOGGER;
use crate::widget::inner::TuiWidgetInnerState;
use crate::TuiWidgetState;
use log::Level;
use log::LevelFilter;
fn advance_levelfilter(levelfilter: LevelFilter) -> (Option<LevelFilter>, Option<LevelFilter>) {
match levelfilter {
LevelFilter::Trace => (None, Some(LevelFilter::Debug)),
LevelFilter::Debug => (Some(LevelFilter::Trace), Some(LevelFilter::Info)),
LevelFilter::Info => (Some(LevelFilter::Debug), Some(LevelFilter::Warn)),
LevelFilter::Warn => (Some(LevelFilter::Info), Some(LevelFilter::Error)),
LevelFilter::Error => (Some(LevelFilter::Warn), Some(LevelFilter::Off)),
LevelFilter::Off => (Some(LevelFilter::Error), None),
}
}
/// This is the definition for the TuiLoggerTargetWidget,
/// which allows configuration of the logger system and selection of log messages.
pub struct TuiLoggerTargetWidget<'b> {
block: Option<Block<'b>>,
/// Base style of the widget
style: Style,
style_show: Style,
style_hide: Style,
style_off: Option<Style>,
highlight_style: Style,
state: Arc<Mutex<TuiWidgetInnerState>>,
targets: Vec<String>,
}
impl<'b> Default for TuiLoggerTargetWidget<'b> {
fn default() -> TuiLoggerTargetWidget<'b> {
TuiLoggerTargetWidget {
block: None,
style: Default::default(),
style_off: None,
style_hide: Style::default(),
style_show: Style::default().add_modifier(Modifier::REVERSED),
highlight_style: Style::default().add_modifier(Modifier::REVERSED),
state: Arc::new(Mutex::new(TuiWidgetInnerState::new())),
targets: vec![],
}
}
}
impl<'b> TuiLoggerTargetWidget<'b> {
pub fn block(mut self, block: Block<'b>) -> TuiLoggerTargetWidget<'b> {
self.block = Some(block);
self
}
pub fn opt_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
if let Some(s) = style {
self.style = s;
}
self
}
pub fn opt_style_off(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
if style.is_some() {
self.style_off = style;
}
self
}
pub fn opt_style_hide(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
if let Some(s) = style {
self.style_hide = s;
}
self
}
pub fn opt_style_show(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
if let Some(s) = style {
self.style_show = s;
}
self
}
pub fn opt_highlight_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidget<'b> {
if let Some(s) = style {
self.highlight_style = s;
}
self
}
pub fn style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
self.style = style;
self
}
pub fn style_off(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
self.style_off = Some(style);
self
}
pub fn style_hide(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
self.style_hide = style;
self
}
pub fn style_show(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
self.style_show = style;
self
}
pub fn highlight_style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
self.highlight_style = style;
self
}
pub(crate) fn inner_state(
mut self,
state: Arc<Mutex<TuiWidgetInnerState>>,
) -> TuiLoggerTargetWidget<'b> {
self.state = state;
self
}
pub fn state(mut self, state: &TuiWidgetState) -> TuiLoggerTargetWidget<'b> {
self.state = state.clone_state();
self
}
}
impl<'b> Widget for TuiLoggerTargetWidget<'b> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
let list_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if list_area.width < 8 || list_area.height < 1 {
return;
}
let la_left = list_area.left();
let la_top = list_area.top();
let la_width = list_area.width as usize;
{
let inner = &TUI_LOGGER.inner.lock();
let hot_targets = &inner.targets;
let mut state = self.state.lock();
let hide_off = state.hide_off;
let offset = state.offset;
let focus_selected = state.focus_selected;
{
let targets = &mut state.config;
targets.merge(hot_targets);
self.targets.clear();
for (t, levelfilter) in targets.iter() {
if hide_off && levelfilter == &LevelFilter::Off {
continue;
}
self.targets.push(t.clone());
}
self.targets.sort();
}
state.nr_items = self.targets.len();
if state.selected >= state.nr_items {
state.selected = state.nr_items.max(1) - 1;
}
if state.selected < state.nr_items {
state.opt_selected_target = Some(self.targets[state.selected].clone());
let t = &self.targets[state.selected];
let (more, less) = if let Some(levelfilter) = state.config.get(t) {
advance_levelfilter(levelfilter)
} else {
(None, None)
};
state.opt_selected_visibility_less = less;
state.opt_selected_visibility_more = more;
let (more, less) = if let Some(levelfilter) = hot_targets.get(t) {
advance_levelfilter(levelfilter)
} else {
(None, None)
};
state.opt_selected_recording_less = less;
state.opt_selected_recording_more = more;
}
let list_height = (list_area.height as usize).min(self.targets.len());
let offset = if list_height > self.targets.len() {
0
} else if state.selected < state.nr_items {
let sel = state.selected;
if sel >= offset + list_height {
// selected is below visible list range => make it the bottom
sel - list_height + 1
} else if sel.min(offset) + list_height > self.targets.len() {
self.targets.len() - list_height
} else {
sel.min(offset)
}
} else {
0
};
state.offset = offset;
let targets = &(&state.config);
let default_level = inner.default;
for i in 0..list_height {
let t = &self.targets[i + offset];
// Comment in relation to issue #69:
// Widgets maintain their own list of level filters per target.
// These lists are not forwarded to the TUI_LOGGER, but kept widget private.
// Example: This widget's private list contains a target named "not_yet",
// and the application hasn't logged an entry with target "not_yet".
// If displaying the target list, then "not_yet" will be only present in target,
// but not in hot_targets. In issue #69 the problem has been, that
// `hot_targets.get(t).unwrap()` has caused a panic. Which is to be expected.
// The remedy is to use unwrap_or with default_level.
let hot_level_filter = hot_targets.get(t).unwrap_or(default_level);
let level_filter = targets.get(t).unwrap_or(default_level);
for (j, sym, lev) in &[
(0, "E", Level::Error),
(1, "W", Level::Warn),
(2, "I", Level::Info),
(3, "D", Level::Debug),
(4, "T", Level::Trace),
] {
if let Some(cell) = buf.cell_mut((la_left + j, la_top + i as u16)) {
let cell_style = if hot_level_filter >= *lev {
if level_filter >= *lev {
if !focus_selected || i + offset == state.selected {
self.style_show
} else {
self.style_hide
}
} else {
self.style_hide
}
} else if let Some(style_off) = self.style_off {
style_off
} else {
cell.set_symbol(" ");
continue;
};
cell.set_style(cell_style);
cell.set_symbol(sym);
}
}
buf.set_stringn(la_left + 5, la_top + i as u16, ":", la_width, self.style);
buf.set_stringn(
la_left + 6,
la_top + i as u16,
t,
la_width,
if i + offset == state.selected {
self.highlight_style
} else {
self.style
},
);
}
}
}
}
================================================
FILE: tests/empty_log.rs
================================================
use log::*;
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
use tui_logger::*;
#[test]
fn test_panic_on_empty_log() {
let _ = init_logger(LevelFilter::Trace);
set_default_level(LevelFilter::Trace);
info!("");
move_events();
let backend = TestBackend::new(40, 3);
let mut terminal = Terminal::new(backend).unwrap();
let res = terminal.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default().output_timestamp(None);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 40,
height: 3,
},
);
});
assert!(res.is_ok());
}
================================================
FILE: tests/envfilter.rs
================================================
use log::*;
use ratatui::{backend::TestBackend, buffer::Buffer, layout::Rect, Terminal};
use tui_logger::*;
#[cfg(test)]
mod tests {
use super::*; // Import the functions from the parent module
#[test]
fn test_formatter() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Off);
warn!("Message"); // This is suppressed due to LevelFilter::Off
move_events();
set_env_filter_from_string("envfilter=info");
warn!("Message");
move_events();
info!("Message");
move_events();
remove_env_filter(); // Ensure the level has been stored in the hot_select hashtable
info!("Message"); // Default filter would be Off
move_events();
let backend = TestBackend::new(40, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default().output_timestamp(None);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 40,
height: 8,
},
);
})
.unwrap();
let expected = Buffer::with_lines([
"WARN :envfilter::tests:tests/envfilter.r",
" s:17:Message ",
"INFO :envfilter::tests:tests/envfilter.r",
" s:19:Message ",
"INFO :envfilter::tests:tests/envfilter.r",
" s:22:Message ",
" ",
" ",
]);
//expected.set_style(Rect::new(0, 0, 40, 2), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}
}
================================================
FILE: tests/formatter_wrap.rs
================================================
use log::*;
use ratatui::{backend::TestBackend, buffer::Buffer, layout::Rect, Terminal};
use tui_logger::*;
#[cfg(test)]
mod tests {
use super::*; // Import the functions from the parent module
#[test]
fn test_formatter() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Trace);
info!("Message");
move_events();
let backend = TestBackend::new(40, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default().output_timestamp(None);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 40,
height: 3,
},
);
})
.unwrap();
let expected = Buffer::with_lines([
"INFO :formatter_wrap::tests:tests/format",
" ter_wrap.rs:14:Message ",
" ",
]);
//expected.set_style(Rect::new(0, 0, 40, 2), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}
}
================================================
FILE: tests/scroll.rs
================================================
use log::*;
use ratatui::text::{Line, Span};
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
Terminal,
};
use std::borrow::Cow;
use std::{thread, time};
use tui_logger::*;
pub struct TestFormatter {}
impl LogFormatter for TestFormatter {
fn min_width(&self) -> u16 {
1
}
fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let mut spans: Vec<Span> = Vec::new();
let style = Style::new().reversed();
let msg = evt.msg().lines().rev().collect::<Vec<&str>>().join(" ");
spans.push(Span {
style,
content: Cow::Owned(format!("Hello {}", msg)),
});
let line = Line::from(spans);
lines.push(line);
lines
}
}
#[cfg(test)]
mod tests {
use super::*; // Import the functions from the parent module
#[test]
fn test_scroll() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Trace);
let state = TuiWidgetState::new();
info!("0");
thread::sleep(time::Duration::from_millis(10));
info!("1");
thread::sleep(time::Duration::from_millis(10));
info!("2");
thread::sleep(time::Duration::from_millis(10));
info!("3");
thread::sleep(time::Duration::from_millis(10));
info!("4");
thread::sleep(time::Duration::from_millis(10));
info!("5");
thread::sleep(time::Duration::from_millis(10));
info!("6");
move_events();
println!("Initial draw");
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines(["Hello 4 ", "Hello 5 ", "Hello 6 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 3 ", "Hello 4 ", "Hello 5 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 2 ", "Hello 3 ", "Hello 4 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 1 ", "Hello 2 ", "Hello 3 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 0 ", "Hello 1 ", "Hello 2 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up at top");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 0 ", "Hello 1 ", "Hello 2 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll down");
state.transition(TuiWidgetEvent::NextPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 1 ", "Hello 2 ", "Hello 3 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll down");
state.transition(TuiWidgetEvent::NextPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 2 ", "Hello 3 ", "Hello 4 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll down");
state.transition(TuiWidgetEvent::NextPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 3 ", "Hello 4 ", "Hello 5 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll down");
state.transition(TuiWidgetEvent::NextPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 4 ", "Hello 5 ", "Hello 6 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll down at bottom");
state.transition(TuiWidgetEvent::NextPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 4 ", "Hello 5 ", "Hello 6 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}
}
================================================
FILE: tests/scroll_long_wrap.rs
================================================
use log::*;
use ratatui::text::{Line, Span};
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
Terminal,
};
use std::borrow::Cow;
use std::{thread, time};
use tui_logger::*;
pub struct TestFormatter {}
impl LogFormatter for TestFormatter {
fn min_width(&self) -> u16 {
1
}
fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let style = Style::new().reversed();
let msg = evt.msg().lines().rev().collect::<Vec<&str>>().join(" ");
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!("Hello {}", msg)),
});
let line = Line::from(spans);
lines.push(line);
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!(" wrap {} 1", msg)),
});
let line = Line::from(spans);
lines.push(line);
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!(" wrap {} 2", msg)),
});
let line = Line::from(spans);
lines.push(line);
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!(" wrap {} 3", msg)),
});
let line = Line::from(spans);
lines.push(line);
lines
}
}
#[cfg(test)]
mod tests {
use super::*; // Import the functions from the parent module
#[test]
fn test_scroll() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Trace);
let state = TuiWidgetState::new();
info!("0");
thread::sleep(time::Duration::from_millis(10));
info!("1");
thread::sleep(time::Duration::from_millis(10));
info!("2");
thread::sleep(time::Duration::from_millis(10));
move_events();
println!("Initial draw");
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines([" wrap 2 1 ", " wrap 2 2 ", " wrap 2 3 "]);
expected.set_style(Rect::new(0, 0, 9, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 2 ", " wrap 2 1 ", " wrap 2 2 "]);
expected.set_style(Rect::new(0, 0, 7, 1), Style::new().reversed());
expected.set_style(Rect::new(0, 1, 9, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines([" wrap 1 3 ", "Hello 2 ", " wrap 2 1 "]);
expected.set_style(Rect::new(0, 0, 9, 1), Style::new().reversed());
expected.set_style(Rect::new(0, 1, 7, 2), Style::new().reversed());
expected.set_style(Rect::new(0, 2, 9, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
}
}
================================================
FILE: tests/scroll_wrap.rs
================================================
use log::*;
use ratatui::text::{Line, Span};
use ratatui::{
backend::TestBackend,
buffer::Buffer,
layout::Rect,
style::{Style, Stylize},
Terminal,
};
use std::borrow::Cow;
use std::{thread, time};
use tui_logger::*;
pub struct TestFormatter {}
impl LogFormatter for TestFormatter {
fn min_width(&self) -> u16 {
1
}
fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
let mut lines = Vec::new();
let style = Style::new().reversed();
let msg = evt.msg().lines().rev().collect::<Vec<&str>>().join(" ");
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!("Hello {}", msg)),
});
let line = Line::from(spans);
lines.push(line);
let mut spans: Vec<Span> = Vec::new();
spans.push(Span {
style,
content: Cow::Owned(format!(" wrap {}", msg)),
});
let line = Line::from(spans);
lines.push(line);
lines
}
}
#[cfg(test)]
mod tests {
use super::*; // Import the functions from the parent module
#[test]
fn test_scroll() {
init_logger(LevelFilter::Trace).unwrap();
set_default_level(LevelFilter::Trace);
let state = TuiWidgetState::new();
info!("0");
thread::sleep(time::Duration::from_millis(10));
info!("1");
thread::sleep(time::Duration::from_millis(10));
info!("2");
thread::sleep(time::Duration::from_millis(10));
info!("3");
thread::sleep(time::Duration::from_millis(10));
info!("4");
thread::sleep(time::Duration::from_millis(10));
info!("5");
thread::sleep(time::Duration::from_millis(10));
info!("6");
move_events();
println!("Initial draw");
let backend = TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
let mut expected = Buffer::with_lines([" wrap 5 ", "Hello 6 ", " wrap 6 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 5 ", " wrap 5 ", "Hello 6 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines([" wrap 4 ", "Hello 5 ", " wrap 5 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines(["Hello 4 ", " wrap 4 ", "Hello 5 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
Rect {
x: 0,
y: 0,
width: 10,
height: 3,
},
);
})
.unwrap();
expected = Buffer::with_lines([" wrap 3 ", "Hello 4 ", " wrap 4 "]);
expected.set_style(Rect::new(0, 0, 7, 3), Style::new().reversed());
terminal.backend().assert_buffer(&expected);
println!("Scroll up");
state.transition(TuiWidgetEvent::PrevPageKey);
terminal
.draw(|f| {
let tui_logger_widget = TuiLoggerWidget::default()
.formatter(Box::new(TestFormatter {}))
.state(&state);
f.render_widget(
tui_logger_widget,
gitextract_gkuhw83x/
├── .github/
│ ├── FUNDING.yml
│ └── workflows/
│ ├── build_examples.yml
│ ├── build_examples_latest.yml
│ ├── cargo_test.yml
│ ├── docs.yml
│ └── semver_checks.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── DEV_NOTES.md
├── LICENSE
├── README.md
├── bacon.toml
├── doc/
│ ├── demo-short.tape
│ └── demo.tape
├── examples/
│ ├── demo.rs
│ └── slog.rs_outdated
├── src/
│ ├── circular.rs
│ ├── config/
│ │ ├── level_config.rs
│ │ └── mod.rs
│ ├── file.rs
│ ├── lib.rs
│ ├── logger/
│ │ ├── api.rs
│ │ ├── fast_hash.rs
│ │ ├── inner.rs
│ │ └── mod.rs
│ ├── slog.rs
│ ├── tracing_subscriber.rs
│ └── widget/
│ ├── inner.rs
│ ├── logformatter.rs
│ ├── mod.rs
│ ├── smart.rs
│ ├── standard.rs
│ ├── standard_formatter.rs
│ └── target.rs
└── tests/
├── empty_log.rs
├── envfilter.rs
├── formatter_wrap.rs
├── scroll.rs
├── scroll_long_wrap.rs
├── scroll_wrap.rs
└── simple.rs
SYMBOL INDEX (236 symbols across 22 files)
FILE: examples/demo.rs
type App (line 19) | struct App {
method new (line 104) | pub fn new() -> App {
method start (line 124) | pub fn start<B: Backend>(mut self, terminal: &mut Terminal<B>) -> Resu...
method run (line 140) | fn run<B: Backend>(
method update_progress_bar (line 158) | fn update_progress_bar(&mut self, event: AppEvent, value: Option<u16>) {
method handle_ui_event (line 166) | fn handle_ui_event(&mut self, event: Event) {
method selected_state (line 199) | fn selected_state(&mut self) -> &mut TuiWidgetState {
method next_tab (line 203) | fn next_tab(&mut self) {
method draw (line 207) | fn draw<B: Backend>(&mut self, terminal: &mut Terminal<B>) -> Result<(...
type AppMode (line 28) | enum AppMode {
type AppEvent (line 35) | enum AppEvent {
type MyLogFormatter (line 41) | struct MyLogFormatter {}
method min_width (line 43) | fn min_width(&self) -> u16 {
method format (line 46) | fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
function main (line 79) | fn main() {
function progress_task (line 216) | fn progress_task(tx: mpsc::Sender<AppEvent>) -> anyhow::Result<()> {
function background_task (line 230) | fn background_task() {
function background_task2 (line 242) | fn background_task2() {
function heart_task (line 250) | fn heart_task() {
method render (line 260) | fn render(self, area: Rect, buf: &mut Buffer) {
function init_terminal (line 364) | pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
function restore_terminal (line 372) | pub fn restore_terminal() -> io::Result<()> {
function input_thread (line 378) | pub fn input_thread(tx_event: mpsc::Sender<AppEvent>) -> anyhow::Result<...
function init_terminal (line 399) | pub fn init_terminal() -> io::Result<Terminal<impl Backend>> {
function restore_terminal (line 408) | pub fn restore_terminal() -> io::Result<()> {
function input_thread (line 413) | pub fn input_thread(tx_event: mpsc::Sender<AppEvent>) -> anyhow::Result<...
FILE: src/circular.rs
type CircularBuffer (line 55) | pub struct CircularBuffer<T> {
function new (line 62) | pub fn new(max_depth: usize) -> CircularBuffer<T> {
function len (line 69) | pub fn len(&self) -> usize {
function is_empty (line 72) | pub fn is_empty(&self) -> bool {
function capacity (line 75) | pub fn capacity(&self) -> usize {
function first_index (line 79) | pub fn first_index(&self) -> Option<usize> {
function last_index (line 88) | pub fn last_index(&self) -> Option<usize> {
function element_at_index (line 95) | pub fn element_at_index(&self, index: usize) -> Option<&T> {
function push (line 108) | pub fn push(&mut self, elem: T) {
function take (line 118) | pub fn take(&mut self) -> Vec<T> {
function total_elements (line 133) | pub fn total_elements(&self) -> usize {
function has_wrapped (line 137) | pub fn has_wrapped(&self) -> bool {
function iter (line 142) | pub fn iter(&mut self) -> iter::Chain<std::slice::Iter<'_, T>, std::slic...
function rev_iter (line 156) | pub fn rev_iter(
function circular_buffer (line 179) | fn circular_buffer() {
function circular_buffer_rev (line 341) | fn circular_buffer_rev() {
function total_elements (line 475) | fn total_elements() {
function has_wrapped (line 487) | fn has_wrapped() {
function take (line 499) | fn take() {
FILE: src/config/level_config.rs
type LevelConfig (line 14) | pub struct LevelConfig {
method new (line 22) | pub fn new() -> LevelConfig {
method set (line 31) | pub fn set(&mut self, target: &str, level: LevelFilter) {
method set_default_display_level (line 43) | pub fn set_default_display_level(&mut self, level: LevelFilter) {
method get_default_display_level (line 47) | pub fn get_default_display_level(&self) -> Option<LevelFilter> {
method keys (line 51) | pub fn keys(&self) -> Keys<'_, String, LevelFilter> {
method get (line 55) | pub fn get(&self, target: &str) -> Option<LevelFilter> {
method iter (line 59) | pub fn iter(&self) -> Iter<'_, String, LevelFilter> {
method merge (line 67) | pub(crate) fn merge(&mut self, origin: &LevelConfig) {
FILE: src/file.rs
type TuiLoggerFile (line 6) | pub struct TuiLoggerFile {
method new (line 17) | pub fn new(fname: &str) -> Self {
method output_target (line 32) | pub fn output_target(mut self, enabled: bool) -> Self {
method output_file (line 36) | pub fn output_file(mut self, enabled: bool) -> Self {
method output_line (line 40) | pub fn output_line(mut self, enabled: bool) -> Self {
method output_timestamp (line 44) | pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
method output_separator (line 48) | pub fn output_separator(mut self, sep: char) -> Self {
method output_level (line 52) | pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> ...
FILE: src/logger/api.rs
type TuiLoggerError (line 14) | pub enum TuiLoggerError {
method description (line 19) | fn description(&self) -> &str {
method cause (line 25) | fn cause(&self) -> Option<&dyn std::error::Error> {
method fmt (line 33) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
function init_logger (line 42) | pub fn init_logger(max_level: LevelFilter) -> Result<(), TuiLoggerError> {
function set_hot_buffer_depth (line 65) | pub fn set_hot_buffer_depth(depth: usize) {
function set_buffer_depth (line 71) | pub fn set_buffer_depth(depth: usize) {
function set_log_file (line 76) | pub fn set_log_file(file_options: TuiLoggerFile) {
function set_default_level (line 81) | pub fn set_default_level(levelfilter: LevelFilter) {
function remove_env_filter (line 87) | pub fn remove_env_filter() {
function set_env_filter (line 92) | fn set_env_filter(filter1: env_filter::Filter, filter2: env_filter::Filt...
function set_env_filter_from_string (line 100) | pub fn set_env_filter_from_string(filterstring: &str) {
function set_env_filter_from_env (line 111) | pub fn set_env_filter_from_env(env_name: Option<&str>) {
function set_level_for_target (line 125) | pub fn set_level_for_target(target: &str, levelfilter: LevelFilter) {
function move_events (line 133) | pub fn move_events() {
type Drain (line 139) | pub struct Drain;
method new (line 143) | pub fn new() -> Self {
method log (line 147) | pub fn log(&self, record: &Record) {
FILE: src/logger/fast_hash.rs
function fast_str_hash (line 2) | pub fn fast_str_hash(s: &str) -> u64 {
FILE: src/logger/inner.rs
type TuiLoggerLevelOutput (line 15) | pub enum TuiLoggerLevelOutput {
type HotSelect (line 20) | pub(crate) struct HotSelect {
type HotLog (line 25) | pub(crate) struct HotLog {
type StringOrStatic (line 30) | enum StringOrStatic {
method as_str (line 35) | fn as_str(&self) -> &str {
type ExtLogRecord (line 43) | pub struct ExtLogRecord {
method target (line 54) | pub fn target(&self) -> &str {
method file (line 58) | pub fn file(&self) -> Option<&str> {
method module_path (line 62) | pub fn module_path(&self) -> Option<&str> {
method msg (line 66) | pub fn msg(&self) -> &str {
method from (line 69) | fn from(record: &Record) -> Self {
method overrun (line 96) | fn overrun(timestamp: Zoned, total: usize, elements: usize) -> Self {
type TuiLoggerInner (line 113) | pub(crate) struct TuiLoggerInner {
type TuiLogger (line 122) | pub struct TuiLogger {
method move_events (line 128) | pub fn move_events(&self) {
method raw_log (line 296) | pub fn raw_log(&self, record: &Record) {
method enabled (line 274) | fn enabled(&self, metadata: &Metadata) -> bool {
method log (line 286) | fn log(&self, record: &Record) {
method flush (line 292) | fn flush(&self) {}
FILE: src/slog.rs
function slog_drain (line 9) | pub fn slog_drain() -> TuiSlogDrain {
type Ksv (line 15) | struct Ksv<W: io::Write> {
function new (line 20) | fn new(io: W) -> Self {
function into_inner (line 24) | fn into_inner(self) -> W {
function emit_arguments (line 30) | fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> sl...
type LazyLogString (line 37) | struct LazyLogString<'a> {
function new (line 43) | fn new(info: &'a slog::Record, logger_values: &'a slog::OwnedKVList) -> ...
function fmt (line 52) | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
type TuiSlogDrain (line 86) | pub struct TuiSlogDrain;
type Ok (line 89) | type Ok = ();
type Err (line 90) | type Err = io::Error;
method log (line 92) | fn log(&self, info: &slog::Record, logger_values: &slog::OwnedKVList) ->...
FILE: src/tracing_subscriber.rs
type ToStringVisitor (line 11) | struct ToStringVisitor<'a>(BTreeMap<&'a str, String>);
function fmt (line 14) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
function record_f64 (line 26) | fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
function record_i64 (line 31) | fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
function record_u64 (line 36) | fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
function record_bool (line 41) | fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
function record_str (line 46) | fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
function record_error (line 51) | fn record_error(
function record_debug (line 60) | fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn st...
type SpanAttributes (line 90) | struct SpanAttributes {
type TuiTracingSubscriberLayer (line 94) | pub struct TuiTracingSubscriberLayer;
method on_new_span (line 100) | fn on_new_span(
method on_event (line 116) | fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber...
FILE: src/widget/inner.rs
type LinePointer (line 9) | pub(crate) struct LinePointer {
type TuiWidgetState (line 16) | pub struct TuiWidgetState {
method new (line 21) | pub fn new() -> TuiWidgetState {
method set_default_display_level (line 26) | pub fn set_default_display_level(self, levelfilter: LevelFilter) -> Tu...
method set_level_for_target (line 33) | pub fn set_level_for_target(self, target: &str, levelfilter: LevelFilt...
method transition (line 37) | pub fn transition(&self, event: TuiWidgetEvent) {
method clone_state (line 40) | pub fn clone_state(&self) -> Arc<Mutex<TuiWidgetInnerState>> {
type TuiWidgetEvent (line 46) | pub enum TuiWidgetEvent {
type TuiWidgetInnerState (line 62) | pub struct TuiWidgetInnerState {
method new (line 80) | pub fn new() -> TuiWidgetInnerState {
method transition (line 83) | fn transition(&mut self, event: TuiWidgetEvent) {
FILE: src/widget/logformatter.rs
type LogFormatter (line 4) | pub trait LogFormatter: Send + Sync {
method min_width (line 5) | fn min_width(&self) -> u16;
method format (line 10) | fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>>;
FILE: src/widget/smart.rs
type TuiLoggerSmartWidget (line 25) | pub struct TuiLoggerSmartWidget<'a> {
method default (line 50) | fn default() -> Self {
function highlight_style (line 78) | pub fn highlight_style(mut self, style: Style) -> Self {
function border_style (line 82) | pub fn border_style(mut self, style: Style) -> Self {
function border_type (line 86) | pub fn border_type(mut self, border_type: BorderType) -> Self {
function style (line 90) | pub fn style(mut self, style: Style) -> Self {
function style_error (line 94) | pub fn style_error(mut self, style: Style) -> Self {
function style_warn (line 98) | pub fn style_warn(mut self, style: Style) -> Self {
function style_info (line 102) | pub fn style_info(mut self, style: Style) -> Self {
function style_trace (line 106) | pub fn style_trace(mut self, style: Style) -> Self {
function style_debug (line 110) | pub fn style_debug(mut self, style: Style) -> Self {
function style_off (line 114) | pub fn style_off(mut self, style: Style) -> Self {
function style_hide (line 118) | pub fn style_hide(mut self, style: Style) -> Self {
function style_show (line 122) | pub fn style_show(mut self, style: Style) -> Self {
function output_separator (line 128) | pub fn output_separator(mut self, sep: char) -> Self {
function output_timestamp (line 138) | pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
function output_level (line 149) | pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Se...
function output_target (line 156) | pub fn output_target(mut self, enabled: bool) -> Self {
function output_file (line 163) | pub fn output_file(mut self, enabled: bool) -> Self {
function output_line (line 170) | pub fn output_line(mut self, enabled: bool) -> Self {
function title_target (line 174) | pub fn title_target<T>(mut self, title: T) -> Self
function title_log (line 181) | pub fn title_log<T>(mut self, title: T) -> Self
function state (line 188) | pub fn state(mut self, state: &TuiWidgetState) -> Self {
method render (line 195) | fn render(self, area: Rect, buf: &mut Buffer) {
FILE: src/widget/standard.rs
type TuiLoggerWidget (line 19) | pub struct TuiLoggerWidget<'b> {
method default (line 39) | fn default() -> TuiLoggerWidget<'b> {
function block (line 60) | pub fn block(mut self, block: Block<'b>) -> Self {
function opt_formatter (line 64) | pub fn opt_formatter(mut self, formatter: Option<Box<dyn LogFormatter>>)...
function formatter (line 68) | pub fn formatter(mut self, formatter: Box<dyn LogFormatter>) -> Self {
function opt_style (line 72) | pub fn opt_style(mut self, style: Option<Style>) -> Self {
function opt_style_error (line 78) | pub fn opt_style_error(mut self, style: Option<Style>) -> Self {
function opt_style_warn (line 84) | pub fn opt_style_warn(mut self, style: Option<Style>) -> Self {
function opt_style_info (line 90) | pub fn opt_style_info(mut self, style: Option<Style>) -> Self {
function opt_style_trace (line 96) | pub fn opt_style_trace(mut self, style: Option<Style>) -> Self {
function opt_style_debug (line 102) | pub fn opt_style_debug(mut self, style: Option<Style>) -> Self {
function style (line 108) | pub fn style(mut self, style: Style) -> Self {
function style_error (line 112) | pub fn style_error(mut self, style: Style) -> Self {
function style_warn (line 116) | pub fn style_warn(mut self, style: Style) -> Self {
function style_info (line 120) | pub fn style_info(mut self, style: Style) -> Self {
function style_trace (line 124) | pub fn style_trace(mut self, style: Style) -> Self {
function style_debug (line 128) | pub fn style_debug(mut self, style: Style) -> Self {
function opt_output_separator (line 132) | pub fn opt_output_separator(mut self, opt_sep: Option<char>) -> Self {
function output_separator (line 140) | pub fn output_separator(mut self, sep: char) -> Self {
function opt_output_timestamp (line 144) | pub fn opt_output_timestamp(mut self, opt_fmt: Option<Option<String>>) -...
function output_timestamp (line 156) | pub fn output_timestamp(mut self, fmt: Option<String>) -> Self {
function opt_output_level (line 160) | pub fn opt_output_level(mut self, opt_fmt: Option<Option<TuiLoggerLevelO...
function output_level (line 173) | pub fn output_level(mut self, level: Option<TuiLoggerLevelOutput>) -> Se...
function opt_output_target (line 177) | pub fn opt_output_target(mut self, opt_enabled: Option<bool>) -> Self {
function output_target (line 186) | pub fn output_target(mut self, enabled: bool) -> Self {
function opt_output_file (line 190) | pub fn opt_output_file(mut self, opt_enabled: Option<bool>) -> Self {
function output_file (line 199) | pub fn output_file(mut self, enabled: bool) -> Self {
function opt_output_line (line 203) | pub fn opt_output_line(mut self, opt_enabled: Option<bool>) -> Self {
function output_line (line 212) | pub fn output_line(mut self, enabled: bool) -> Self {
function inner_state (line 216) | pub fn inner_state(mut self, state: Arc<Mutex<TuiWidgetInnerState>>) -> ...
function state (line 220) | pub fn state(mut self, state: &TuiWidgetState) -> Self {
function next_event (line 224) | fn next_event<'a>(
method render (line 283) | fn render(mut self, area: Rect, buf: &mut Buffer) {
FILE: src/widget/standard_formatter.rs
type LogStandardFormatter (line 9) | pub struct LogStandardFormatter {
method append_wrapped_line (line 27) | fn append_wrapped_line(
method min_width (line 70) | fn min_width(&self) -> u16 {
method format (line 73) | fn format(&self, width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
FILE: src/widget/target.rs
function advance_levelfilter (line 17) | fn advance_levelfilter(levelfilter: LevelFilter) -> (Option<LevelFilter>...
type TuiLoggerTargetWidget (line 30) | pub struct TuiLoggerTargetWidget<'b> {
method default (line 42) | fn default() -> TuiLoggerTargetWidget<'b> {
function block (line 56) | pub fn block(mut self, block: Block<'b>) -> TuiLoggerTargetWidget<'b> {
function opt_style (line 60) | pub fn opt_style(mut self, style: Option<Style>) -> TuiLoggerTargetWidge...
function opt_style_off (line 66) | pub fn opt_style_off(mut self, style: Option<Style>) -> TuiLoggerTargetW...
function opt_style_hide (line 72) | pub fn opt_style_hide(mut self, style: Option<Style>) -> TuiLoggerTarget...
function opt_style_show (line 78) | pub fn opt_style_show(mut self, style: Option<Style>) -> TuiLoggerTarget...
function opt_highlight_style (line 84) | pub fn opt_highlight_style(mut self, style: Option<Style>) -> TuiLoggerT...
function style (line 90) | pub fn style(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
function style_off (line 94) | pub fn style_off(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
function style_hide (line 98) | pub fn style_hide(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
function style_show (line 102) | pub fn style_show(mut self, style: Style) -> TuiLoggerTargetWidget<'b> {
function highlight_style (line 106) | pub fn highlight_style(mut self, style: Style) -> TuiLoggerTargetWidget<...
function inner_state (line 110) | pub(crate) fn inner_state(
function state (line 117) | pub fn state(mut self, state: &TuiWidgetState) -> TuiLoggerTargetWidget<...
method render (line 123) | fn render(mut self, area: Rect, buf: &mut Buffer) {
FILE: tests/empty_log.rs
function test_panic_on_empty_log (line 6) | fn test_panic_on_empty_log() {
FILE: tests/envfilter.rs
function test_formatter (line 10) | fn test_formatter() {
FILE: tests/formatter_wrap.rs
function test_formatter (line 10) | fn test_formatter() {
FILE: tests/scroll.rs
type TestFormatter (line 14) | pub struct TestFormatter {}
method min_width (line 16) | fn min_width(&self) -> u16 {
method format (line 19) | fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
function test_scroll (line 39) | fn test_scroll() {
FILE: tests/scroll_long_wrap.rs
type TestFormatter (line 14) | pub struct TestFormatter {}
method min_width (line 16) | fn min_width(&self) -> u16 {
method format (line 19) | fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
function test_scroll (line 65) | fn test_scroll() {
FILE: tests/scroll_wrap.rs
type TestFormatter (line 14) | pub struct TestFormatter {}
method min_width (line 16) | fn min_width(&self) -> u16 {
method format (line 19) | fn format(&self, _width: usize, evt: &ExtLogRecord) -> Vec<Line<'_>> {
function test_scroll (line 46) | fn test_scroll() {
FILE: tests/simple.rs
type TestFormatter (line 13) | pub struct TestFormatter {}
method min_width (line 15) | fn min_width(&self) -> u16 {
method format (line 18) | fn format(&self, _width: usize, _evt: &ExtLogRecord) -> Vec<Line<'_>> {
function test_simple (line 37) | fn test_simple() {
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (222K chars).
[
{
"path": ".github/FUNDING.yml",
"chars": 929,
"preview": "# These are supported funding model platforms\n\ngithub: [gin66] # Replace with up to 4 GitHub Sponsors-enabled usernames "
},
{
"path": ".github/workflows/build_examples.yml",
"chars": 591,
"preview": "name: Build examples\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]\n\njobs:\n build:\n runs"
},
{
"path": ".github/workflows/build_examples_latest.yml",
"chars": 565,
"preview": "name: Build examples with latest rust version\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]"
},
{
"path": ".github/workflows/cargo_test.yml",
"chars": 368,
"preview": "name: cargo test\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]\n\njobs:\n build:\n runs-on:"
},
{
"path": ".github/workflows/docs.yml",
"chars": 394,
"preview": "name: Documentation\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]\n\njobs:\n check-docs:\n "
},
{
"path": ".github/workflows/semver_checks.yml",
"chars": 296,
"preview": "name: Semver Checks\n\non:\n push:\n branches: [master]\n pull_request:\n branches: [master]\n\njobs:\n semver-checks:\n "
},
{
"path": ".gitignore",
"chars": 40,
"preview": "/target\n**/*.rs.bk\nCargo.lock\n.DS_Store\n"
},
{
"path": "CHANGELOG.md",
"chars": 9071,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "Cargo.toml",
"chars": 1916,
"preview": "[package]\nname = \"tui-logger\"\nversion = \"0.18.2\"\nauthors = [\"Jochen Kiemes <jochen@kiemes.de>\"]\nedition = \"2021\"\nlicense"
},
{
"path": "DEV_NOTES.md",
"chars": 670,
"preview": "# Release process\n\n## Prepare documentation\n\nRun first `cargo rdme` and then decide on `cargo rdme --force`\n\n## Update C"
},
{
"path": "LICENSE",
"chars": 1070,
"preview": "MIT License\n\nCopyright (c) 2024 Jochen Kiemes\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
},
{
"path": "README.md",
"chars": 11938,
"preview": "# tui-logger\n\n<!-- cargo-rdme start -->\n\n## Logger with smart widget for the `tui` and `ratatui` crate\n\n[ algorithm.\npub fn fast_str_hash(s: "
},
{
"path": "src/logger/inner.rs",
"chars": 11067,
"preview": "use crate::logger::fast_hash::fast_str_hash;\nuse crate::{CircularBuffer, LevelConfig, TuiLoggerFile};\nuse env_filter::Fi"
},
{
"path": "src/logger/mod.rs",
"chars": 58,
"preview": "pub mod api;\nmod fast_hash;\nmod inner;\n\npub use inner::*;\n"
},
{
"path": "src/slog.rs",
"chars": 3058,
"preview": "//! `slog` support for `tui-logger`\n\nuse super::TUI_LOGGER;\nuse log::{self, Log, Record};\nuse slog::{self, Drain, KV};\nu"
},
{
"path": "src/tracing_subscriber.rs",
"chars": 4650,
"preview": "//! `tracing-subscriber` support for `tui-logger`\n\nuse super::TUI_LOGGER;\nuse log::{self, Log, Record};\nuse std::collect"
},
{
"path": "src/widget/inner.rs",
"chars": 4780,
"preview": "use std::sync::Arc;\n\nuse log::LevelFilter;\nuse parking_lot::Mutex;\n\nuse crate::{set_level_for_target, LevelConfig};\n\n#[d"
},
{
"path": "src/widget/logformatter.rs",
"chars": 395,
"preview": "use crate::ExtLogRecord;\nuse ratatui::text::Line;\n\npub trait LogFormatter: Send + Sync {\n fn min_width(&self) -> u16;"
},
{
"path": "src/widget/mod.rs",
"chars": 110,
"preview": "pub mod inner;\npub mod logformatter;\npub mod smart;\npub mod standard;\nmod standard_formatter;\npub mod target;\n"
},
{
"path": "src/widget/smart.rs",
"chars": 11111,
"preview": "use crate::widget::logformatter::LogFormatter;\nuse parking_lot::Mutex;\nuse std::sync::Arc;\nuse unicode_segmentation::Uni"
},
{
"path": "src/widget/standard.rs",
"chars": 21097,
"preview": "use crate::widget::logformatter::LogFormatter;\nuse crate::widget::standard_formatter::LogStandardFormatter;\nuse parking_"
},
{
"path": "src/widget/standard_formatter.rs",
"chars": 4461,
"preview": "use crate::logger::TuiLoggerLevelOutput;\nuse crate::widget::logformatter::LogFormatter;\nuse crate::ExtLogRecord;\nuse rat"
},
{
"path": "src/widget/target.rs",
"chars": 9873,
"preview": "use std::sync::Arc;\n\nuse parking_lot::Mutex;\nuse ratatui::{\n buffer::Buffer,\n layout::Rect,\n style::{Modifier, "
},
{
"path": "tests/empty_log.rs",
"chars": 707,
"preview": "use log::*;\nuse ratatui::{backend::TestBackend, layout::Rect, Terminal};\nuse tui_logger::*;\n\n#[test]\nfn test_panic_on_em"
},
{
"path": "tests/envfilter.rs",
"chars": 1947,
"preview": "use log::*;\nuse ratatui::{backend::TestBackend, buffer::Buffer, layout::Rect, Terminal};\nuse tui_logger::*;\n\n#[cfg(test)"
},
{
"path": "tests/formatter_wrap.rs",
"chars": 1298,
"preview": "use log::*;\nuse ratatui::{backend::TestBackend, buffer::Buffer, layout::Rect, Terminal};\nuse tui_logger::*;\n\n#[cfg(test)"
},
{
"path": "tests/scroll.rs",
"chars": 10630,
"preview": "use log::*;\nuse ratatui::text::{Line, Span};\nuse ratatui::{\n backend::TestBackend,\n buffer::Buffer,\n layout::Re"
},
{
"path": "tests/scroll_long_wrap.rs",
"chars": 4779,
"preview": "use log::*;\nuse ratatui::text::{Line, Span};\nuse ratatui::{\n backend::TestBackend,\n buffer::Buffer,\n layout::Re"
},
{
"path": "tests/scroll_wrap.rs",
"chars": 22237,
"preview": "use log::*;\nuse ratatui::text::{Line, Span};\nuse ratatui::{\n backend::TestBackend,\n buffer::Buffer,\n layout::Re"
},
{
"path": "tests/simple.rs",
"chars": 1780,
"preview": "use log::*;\nuse ratatui::text::{Line, Span};\nuse ratatui::{\n backend::TestBackend,\n buffer::Buffer,\n layout::Re"
}
]
About this extraction
This page contains the full source code of the gin66/tui-logger GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (207.5 KB), approximately 50.1k tokens, and a symbol index with 236 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.