mirror of
https://codeberg.org/icewind/logsmash.git
synced 2026-06-03 18:14:11 +02:00
borrowed line parsing
This commit is contained in:
parent
b9c7704699
commit
95e09f0e0c
12 changed files with 61 additions and 62 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
|
@ -876,9 +876,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.10.5"
|
version = "1.10.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
|
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aho-corasick",
|
"aho-corasick",
|
||||||
"memchr",
|
"memchr",
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@
|
||||||
releaseMatrix = buildMatrix releaseTargets;
|
releaseMatrix = buildMatrix releaseTargets;
|
||||||
|
|
||||||
devShells.default = mkShell {
|
devShells.default = mkShell {
|
||||||
nativeBuildInputs = with pkgs; [msrvToolchain rustc bacon cargo-msrv cargo-insta samply];
|
nativeBuildInputs = with pkgs; [msrvToolchain rustc bacon cargo-msrv cargo-insta samply hyperfine];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
10
src/app.rs
10
src/app.rs
|
|
@ -6,20 +6,20 @@ use logsmash_data::StatementList;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
pub struct App {
|
pub struct App<'a> {
|
||||||
pub first_date: OffsetDateTime,
|
pub first_date: OffsetDateTime,
|
||||||
pub last_date: OffsetDateTime,
|
pub last_date: OffsetDateTime,
|
||||||
pub lines: Vec<LogLine>,
|
pub lines: Vec<LogLine<'a>>,
|
||||||
pub log_statements: StatementList,
|
pub log_statements: StatementList,
|
||||||
pub matches: Vec<LogMatch>,
|
pub matches: Vec<LogMatch>,
|
||||||
pub error_count: usize,
|
pub error_count: usize,
|
||||||
pub all: LogMatch,
|
pub all: LogMatch,
|
||||||
pub unmatched: LogMatch,
|
pub unmatched: LogMatch,
|
||||||
pub log_file: LogFile,
|
pub log_file: &'a LogFile,
|
||||||
pub error_lines: Vec<(String, serde_json::Error)>,
|
pub error_lines: Vec<(String, serde_json::Error)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl<'a> App<'a> {
|
||||||
pub fn match_lines(&self) -> usize {
|
pub fn match_lines(&self) -> usize {
|
||||||
let unmatched_line_count = if self.unmatched.lines.is_empty() {
|
let unmatched_line_count = if self.unmatched.lines.is_empty() {
|
||||||
0
|
0
|
||||||
|
|
@ -29,7 +29,7 @@ impl App {
|
||||||
self.matches.len() + 1 + unmatched_line_count
|
self.matches.len() + 1 + unmatched_line_count
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_line(&self, index: usize) -> Option<&str> {
|
pub fn get_line(&self, index: usize) -> Option<&'a str> {
|
||||||
self.log_file.nth(index)
|
self.log_file.nth(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use ahash::AHasher;
|
||||||
use logsmash_data::LogLevel;
|
use logsmash_data::LogLevel;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
use time::format_description::well_known::iso8601::{Config, EncodedConfig, TimePrecision};
|
use time::format_description::well_known::iso8601::{Config, EncodedConfig, TimePrecision};
|
||||||
|
|
@ -16,14 +17,14 @@ pub const TIME_FORMAT: EncodedConfig = Config::DEFAULT
|
||||||
.encode();
|
.encode();
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LogLine {
|
pub struct LogLine<'a> {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub version: TinyAsciiStr<16>,
|
pub version: &'a str,
|
||||||
pub level: LogLevel,
|
pub level: LogLevel,
|
||||||
pub message: String,
|
pub message: Cow<'a, str>,
|
||||||
pub exception: Option<Exception>,
|
pub exception: Option<Exception<'a>>,
|
||||||
pub app: TinyAsciiStr<32>,
|
pub app: &'a str,
|
||||||
#[serde(with = "date")]
|
#[serde(with = "date")]
|
||||||
pub time: OffsetDateTime,
|
pub time: OffsetDateTime,
|
||||||
}
|
}
|
||||||
|
|
@ -76,21 +77,10 @@ mod date {
|
||||||
return Ok(date);
|
return Ok(date);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Err(D::Error::custom(format_args!(
|
Err(D::Error::custom(format_args!(
|
||||||
"Failed to parse date: {}",
|
"Failed to parse date: {}",
|
||||||
str
|
str
|
||||||
)));
|
)))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LogLine {
|
|
||||||
pub fn identity(&self) -> u64 {
|
|
||||||
let mut hasher = AHasher::default();
|
|
||||||
self.message.hash(&mut hasher);
|
|
||||||
self.level.hash(&mut hasher);
|
|
||||||
self.exception.hash(&mut hasher);
|
|
||||||
self.app.hash(&mut hasher);
|
|
||||||
hasher.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,15 +89,24 @@ pub fn format_time(time: OffsetDateTime) -> String {
|
||||||
.unwrap_or_else(|_| "Invalid time".into())
|
.unwrap_or_else(|_| "Invalid time".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogLine {
|
impl<'a> LogLine<'a> {
|
||||||
pub fn display(&self) -> String {
|
pub fn identity(&self) -> u64 {
|
||||||
|
let mut hasher = AHasher::default();
|
||||||
|
self.message.hash(&mut hasher);
|
||||||
|
self.level.hash(&mut hasher);
|
||||||
|
self.exception.hash(&mut hasher);
|
||||||
|
self.app.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn display(&'a self) -> Cow<'a, str> {
|
||||||
if let Some(exception) = self.exception.as_ref() {
|
if let Some(exception) = self.exception.as_ref() {
|
||||||
format!(
|
Cow::Owned(format!(
|
||||||
"{}{}{}({}) - {} line {}",
|
"{}{}{}({}) - {} line {}",
|
||||||
if self.message.starts_with("Exception thrown:") {
|
if self.message.starts_with("Exception thrown:") {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
self.message.as_str()
|
&self.message
|
||||||
},
|
},
|
||||||
if self.message.starts_with("Exception thrown:") {
|
if self.message.starts_with("Exception thrown:") {
|
||||||
""
|
""
|
||||||
|
|
@ -118,23 +117,23 @@ impl LogLine {
|
||||||
exception.message,
|
exception.message,
|
||||||
exception.file,
|
exception.file,
|
||||||
exception.line
|
exception.line
|
||||||
)
|
))
|
||||||
} else {
|
} else {
|
||||||
self.message.clone()
|
Cow::Borrowed(&self.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
#[serde(rename_all = "PascalCase")]
|
#[serde(rename_all = "PascalCase")]
|
||||||
pub struct Exception {
|
pub struct Exception<'a> {
|
||||||
pub message: String,
|
pub message: Cow<'a, str>,
|
||||||
pub exception: String,
|
pub exception: Cow<'a, str>,
|
||||||
pub file: String,
|
pub file: Cow<'a, str>,
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Hash for Exception {
|
impl Hash for Exception<'_> {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
self.message.hash(state);
|
self.message.hash(state);
|
||||||
self.exception.hash(state);
|
self.exception.hash(state);
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ fn main() -> MainResult {
|
||||||
unmatched,
|
unmatched,
|
||||||
all,
|
all,
|
||||||
error_count,
|
error_count,
|
||||||
log_file,
|
log_file: &log_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.profile {
|
if args.profile {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ impl Matcher {
|
||||||
if let Some(exception) = &log.exception {
|
if let Some(exception) = &log.exception {
|
||||||
for log_match in self.matches.iter() {
|
for log_match in self.matches.iter() {
|
||||||
if log_match.line == exception.line
|
if log_match.line == exception.line
|
||||||
&& log_match.exception == Some(exception.exception.as_str())
|
&& log_match.exception == Some(exception.exception.as_ref())
|
||||||
&& exception.file.ends_with(log_match.path)
|
&& exception.file.ends_with(log_match.path)
|
||||||
{
|
{
|
||||||
return Some(MatchResult::Single(log_match.index));
|
return Some(MatchResult::Single(log_match.index));
|
||||||
|
|
@ -68,7 +68,7 @@ impl Matcher {
|
||||||
for log_match in self.matches.iter() {
|
for log_match in self.matches.iter() {
|
||||||
if log_match.has_meaningful_message
|
if log_match.has_meaningful_message
|
||||||
&& log.level.matches(log_match.level)
|
&& log.level.matches(log_match.level)
|
||||||
&& log_match.pattern.is_match(log.message.as_str())
|
&& log_match.pattern.is_match(&log.message)
|
||||||
&& log_match.pattern_length >= best_length
|
&& log_match.pattern_length >= best_length
|
||||||
{
|
{
|
||||||
if log_match.pattern_length > best_length {
|
if log_match.pattern_length > best_length {
|
||||||
|
|
@ -147,7 +147,6 @@ impl MatchResult {
|
||||||
fn test_matcher() {
|
fn test_matcher() {
|
||||||
use crate::logline::Exception;
|
use crate::logline::Exception;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tinystr::TinyAsciiStr;
|
|
||||||
|
|
||||||
const STATEMENTS: &[LoggingStatement] = &[
|
const STATEMENTS: &[LoggingStatement] = &[
|
||||||
LoggingStatement {
|
LoggingStatement {
|
||||||
|
|
@ -195,8 +194,8 @@ fn test_matcher() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(MatchResult::Single(0)),
|
Some(MatchResult::Single(0)),
|
||||||
matcher.match_log(&LogLine {
|
matcher.match_log(&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Error,
|
level: LogLevel::Error,
|
||||||
message: "Not allowed to rename a shared album".into(),
|
message: "Not allowed to rename a shared album".into(),
|
||||||
exception: None,
|
exception: None,
|
||||||
|
|
@ -207,8 +206,8 @@ fn test_matcher() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(MatchResult::List(vec![3, 4])),
|
Some(MatchResult::List(vec![3, 4])),
|
||||||
matcher.match_log(&LogLine {
|
matcher.match_log(&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Error,
|
level: LogLevel::Error,
|
||||||
message: "Not allowed to rename an album".into(),
|
message: "Not allowed to rename an album".into(),
|
||||||
exception: None,
|
exception: None,
|
||||||
|
|
@ -219,8 +218,8 @@ fn test_matcher() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Some(MatchResult::Single(1)),
|
Some(MatchResult::Single(1)),
|
||||||
matcher.match_log(&LogLine {
|
matcher.match_log(&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Error,
|
level: LogLevel::Error,
|
||||||
message: "You are not allowed to edit link shares that you don't own".into(),
|
message: "You are not allowed to edit link shares that you don't own".into(),
|
||||||
exception: None,
|
exception: None,
|
||||||
|
|
@ -231,8 +230,8 @@ fn test_matcher() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
None,
|
None,
|
||||||
matcher.match_log(&LogLine {
|
matcher.match_log(&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Info,
|
level: LogLevel::Info,
|
||||||
message: "You are not allowed to edit link shares that you don't own".into(),
|
message: "You are not allowed to edit link shares that you don't own".into(),
|
||||||
exception: None,
|
exception: None,
|
||||||
|
|
@ -244,8 +243,8 @@ fn test_matcher() {
|
||||||
Some(MatchResult::Single(2)),
|
Some(MatchResult::Single(2)),
|
||||||
matcher.match_log(
|
matcher.match_log(
|
||||||
&LogLine {
|
&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Error,
|
level: LogLevel::Error,
|
||||||
message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
|
message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
|
||||||
exception: None,
|
exception: None,
|
||||||
|
|
@ -258,8 +257,8 @@ fn test_matcher() {
|
||||||
Some(MatchResult::Single(4)),
|
Some(MatchResult::Single(4)),
|
||||||
matcher.match_log(
|
matcher.match_log(
|
||||||
&LogLine {
|
&LogLine {
|
||||||
version: TinyAsciiStr::from_str("29").unwrap(),
|
version: "29",
|
||||||
app: TinyAsciiStr::from_str("core").unwrap(),
|
app: "core",
|
||||||
level: LogLevel::Error,
|
level: LogLevel::Error,
|
||||||
message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
|
message: "Unsupported query value for mimetype: %/text, only values in the format \"mime/type\" or \"mime/%\" are supported".into(),
|
||||||
exception: Some(Exception {
|
exception: Some(Exception {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use ratatui::layout::Constraint;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
|
||||||
pub fn error_list(app: &App) -> ScrollbarTable {
|
pub fn error_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
|
||||||
let header = [Text::from("Error"), Text::from("Line")]
|
let header = [Text::from("Error"), Text::from("Line")]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(Cell::from)
|
.map(Cell::from)
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@ use crate::ui::state::UiPage;
|
||||||
use ratatui::layout::Constraint;
|
use ratatui::layout::Constraint;
|
||||||
use ratatui::prelude::Style;
|
use ratatui::prelude::Style;
|
||||||
use ratatui::style::palette::tailwind;
|
use ratatui::style::palette::tailwind;
|
||||||
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Row, Table};
|
use ratatui::widgets::{Row, Table};
|
||||||
|
|
||||||
pub fn footer(app: &App, page: UiPage) -> Table {
|
pub fn footer<'a>(app: &App<'a>, page: UiPage) -> Table<'a> {
|
||||||
let footer_style = Style::default()
|
let footer_style = Style::default()
|
||||||
.bg(tailwind::BLACK)
|
.bg(tailwind::BLACK)
|
||||||
.fg(tailwind::GREEN.c600);
|
.fg(tailwind::GREEN.c600);
|
||||||
|
|
@ -18,9 +19,9 @@ pub fn footer(app: &App, page: UiPage) -> Table {
|
||||||
|
|
||||||
Table::new(
|
Table::new(
|
||||||
[Row::new([
|
[Row::new([
|
||||||
help(page).to_string(),
|
Text::from(help(page)),
|
||||||
format!("{} unmatched items", app.unmatched.lines.len()),
|
Text::from(format!("{} unmatched items", app.unmatched.lines.len())),
|
||||||
format!("{} parse errors", app.error_count),
|
Text::from(format!("{} parse errors", app.error_count)),
|
||||||
])],
|
])],
|
||||||
widths,
|
widths,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use ratatui::widgets::{Cell, Row};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::iter::{empty, once};
|
use std::iter::{empty, once};
|
||||||
|
|
||||||
pub fn match_list(app: &App) -> ScrollbarTable {
|
pub fn match_list<'a>(app: &'a App<'a>) -> ScrollbarTable<'a> {
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Statement"),
|
Text::from("Statement"),
|
||||||
Text::from("File"),
|
Text::from("File"),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use ratatui::layout::{Alignment, Constraint};
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
|
||||||
pub fn raw_logs<'a>(app: &'a App, lines: &[usize]) -> ScrollbarTable<'a> {
|
pub fn raw_logs<'a>(app: &'a App<'a>, lines: &[usize]) -> ScrollbarTable<'a> {
|
||||||
let lines = lines.iter().copied().map(|i| &app.lines[i]);
|
let lines = lines.iter().copied().map(|i| &app.lines[i]);
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Level"),
|
Text::from("Level"),
|
||||||
|
|
@ -29,10 +29,10 @@ pub fn raw_logs<'a>(app: &'a App, lines: &[usize]) -> ScrollbarTable<'a> {
|
||||||
ScrollbarTable::new(lines.map(log_row), widths).header(header)
|
ScrollbarTable::new(lines.map(log_row), widths).header(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log_row(line: &LogLine) -> Row {
|
fn log_row<'a>(line: &'a LogLine<'a>) -> Row<'a> {
|
||||||
Row::new([
|
Row::new([
|
||||||
Text::from(line.level.as_str()),
|
Text::from(line.level.as_str()),
|
||||||
Text::from(line.app.as_str()),
|
Text::from(line.app),
|
||||||
Text::from(line.display()),
|
Text::from(line.display()),
|
||||||
Text::from(format_time(line.time)).alignment(Alignment::Right),
|
Text::from(format_time(line.time)).alignment(Alignment::Right),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use ratatui::layout::Constraint;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
|
|
||||||
pub fn grouped_lines<'a>(app: &'a App, log_match: &'a LogMatch) -> ScrollbarTable<'a> {
|
pub fn grouped_lines<'a>(app: &'a App<'a>, log_match: &'a LogMatch) -> ScrollbarTable<'a> {
|
||||||
let grouped = &log_match.grouped;
|
let grouped = &log_match.grouped;
|
||||||
let header = [
|
let header = [
|
||||||
Text::from("Level"),
|
Text::from("Level"),
|
||||||
|
|
@ -35,7 +35,7 @@ fn group_row<'a>(app: &'a App, group: &'a GroupedLines) -> Row<'a> {
|
||||||
|
|
||||||
Row::new([
|
Row::new([
|
||||||
Text::from(line.level.as_str()),
|
Text::from(line.level.as_str()),
|
||||||
Text::from(line.app.as_str()),
|
Text::from(line.app),
|
||||||
Text::from(line.display()),
|
Text::from(line.display()),
|
||||||
Text::from(group.sparkline.as_str()),
|
Text::from(group.sparkline.as_str()),
|
||||||
Text::from(group.len().to_string()),
|
Text::from(group.len().to_string()),
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ impl<'a> LogsState<'a> {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct LogState<'a> {
|
pub struct LogState<'a> {
|
||||||
pub trace_len: usize,
|
pub trace_len: usize,
|
||||||
pub log: &'a LogLine,
|
pub log: &'a LogLine<'a>,
|
||||||
pub full_line: FullLogLine,
|
pub full_line: FullLogLine,
|
||||||
pub table_state: ScrollbarTableState,
|
pub table_state: ScrollbarTableState,
|
||||||
pub previous: Box<UiState<'a>>,
|
pub previous: Box<UiState<'a>>,
|
||||||
|
|
@ -97,7 +97,7 @@ impl<'a> UiState<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn process(self, event: UiEvent, app: &'a App) -> (bool, UiState) {
|
pub fn process(self, event: UiEvent, app: &'a App<'a>) -> (bool, UiState) {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(UiState::Quit, _) => (true, UiState::Quit),
|
(UiState::Quit, _) => (true, UiState::Quit),
|
||||||
(_, UiEvent::Quit) => (true, UiState::Quit),
|
(_, UiEvent::Quit) => (true, UiState::Quit),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue