feat(main): added n N and ? motions and improved user subheadings
This commit is contained in:
397
src/main.rs
397
src/main.rs
@@ -151,16 +151,26 @@ struct MesonVersionInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Section {
|
struct SubSection {
|
||||||
name: String,
|
name: String,
|
||||||
collapsed: bool,
|
collapsed: bool,
|
||||||
options: Vec<MesonOption>,
|
options: Vec<MesonOption>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct Section {
|
||||||
|
name: String,
|
||||||
|
collapsed: bool,
|
||||||
|
root_options: Vec<MesonOption>,
|
||||||
|
subsections: Vec<SubSection>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum FlattenedItem {
|
enum FlattenedItem {
|
||||||
Header(usize),
|
SectionHeader(usize),
|
||||||
Option(usize, usize),
|
SectionOption(usize, usize),
|
||||||
|
SubSectionHeader(usize, usize),
|
||||||
|
SubSectionOption(usize, usize, usize),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -173,12 +183,29 @@ enum InputMode {
|
|||||||
OutputViewer,
|
OutputViewer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Copy)]
|
||||||
|
enum SearchDirection {
|
||||||
|
Forward,
|
||||||
|
Backward,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchDirection {
|
||||||
|
fn opposite(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Forward => Self::Backward,
|
||||||
|
Self::Backward => Self::Forward,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
sections: Vec<Section>,
|
sections: Vec<Section>,
|
||||||
flattened_items: Vec<FlattenedItem>,
|
flattened_items: Vec<FlattenedItem>,
|
||||||
state: ListState,
|
state: ListState,
|
||||||
input_mode: InputMode,
|
input_mode: InputMode,
|
||||||
input_buffer: String,
|
input_buffer: String,
|
||||||
|
last_search_query: String,
|
||||||
|
search_direction: SearchDirection,
|
||||||
status_msg: String,
|
status_msg: String,
|
||||||
build_dir: String,
|
build_dir: String,
|
||||||
pending_g: bool,
|
pending_g: bool,
|
||||||
@@ -206,6 +233,8 @@ impl App {
|
|||||||
state: ListState::default(),
|
state: ListState::default(),
|
||||||
input_mode: InputMode::Normal,
|
input_mode: InputMode::Normal,
|
||||||
input_buffer: String::new(),
|
input_buffer: String::new(),
|
||||||
|
last_search_query: String::new(),
|
||||||
|
search_direction: SearchDirection::Forward,
|
||||||
status_msg: "Loading...".to_string(),
|
status_msg: "Loading...".to_string(),
|
||||||
build_dir,
|
build_dir,
|
||||||
pending_g: false,
|
pending_g: false,
|
||||||
@@ -229,7 +258,10 @@ impl App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self) -> bool {
|
fn is_dirty(&self) -> bool {
|
||||||
self.sections.iter().any(|s| s.options.iter().any(|o| o.dirty))
|
self.sections.iter().any(|s| {
|
||||||
|
s.root_options.iter().any(|o| o.dirty) ||
|
||||||
|
s.subsections.iter().any(|sub| sub.options.iter().any(|o| o.dirty))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_flattened(&mut self) {
|
fn update_flattened(&mut self) {
|
||||||
@@ -237,10 +269,18 @@ impl App {
|
|||||||
|
|
||||||
self.flattened_items.clear();
|
self.flattened_items.clear();
|
||||||
for (sec_idx, section) in self.sections.iter().enumerate() {
|
for (sec_idx, section) in self.sections.iter().enumerate() {
|
||||||
self.flattened_items.push(FlattenedItem::Header(sec_idx));
|
self.flattened_items.push(FlattenedItem::SectionHeader(sec_idx));
|
||||||
if !section.collapsed {
|
if !section.collapsed {
|
||||||
for opt_idx in 0..section.options.len() {
|
for opt_idx in 0..section.root_options.len() {
|
||||||
self.flattened_items.push(FlattenedItem::Option(sec_idx, opt_idx));
|
self.flattened_items.push(FlattenedItem::SectionOption(sec_idx, opt_idx));
|
||||||
|
}
|
||||||
|
for (sub_idx, subsection) in section.subsections.iter().enumerate() {
|
||||||
|
self.flattened_items.push(FlattenedItem::SubSectionHeader(sec_idx, sub_idx));
|
||||||
|
if !subsection.collapsed {
|
||||||
|
for opt_idx in 0..subsection.options.len() {
|
||||||
|
self.flattened_items.push(FlattenedItem::SubSectionOption(sec_idx, sub_idx, opt_idx));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -260,7 +300,7 @@ impl App {
|
|||||||
let i = match self.state.selected() {
|
let i = match self.state.selected() {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
if i >= self.flattened_items.len() - 1 {
|
if i >= self.flattened_items.len() - 1 {
|
||||||
0
|
i
|
||||||
} else {
|
} else {
|
||||||
i + 1
|
i + 1
|
||||||
}
|
}
|
||||||
@@ -275,7 +315,7 @@ impl App {
|
|||||||
let i = match self.state.selected() {
|
let i = match self.state.selected() {
|
||||||
Some(i) => {
|
Some(i) => {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
self.flattened_items.len() - 1
|
0
|
||||||
} else {
|
} else {
|
||||||
i - 1
|
i - 1
|
||||||
}
|
}
|
||||||
@@ -301,12 +341,15 @@ impl App {
|
|||||||
fn toggle_fold(&mut self) {
|
fn toggle_fold(&mut self) {
|
||||||
if let Some(idx) = self.state.selected() {
|
if let Some(idx) = self.state.selected() {
|
||||||
match self.flattened_items[idx] {
|
match self.flattened_items[idx] {
|
||||||
FlattenedItem::Header(sec_idx) => {
|
FlattenedItem::SectionHeader(sec_idx) => {
|
||||||
self.sections[sec_idx].collapsed = !self.sections[sec_idx].collapsed;
|
self.sections[sec_idx].collapsed = !self.sections[sec_idx].collapsed;
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
FlattenedItem::Option(_sec_idx, _) => {
|
FlattenedItem::SubSectionHeader(sec_idx, sub_idx) => {
|
||||||
|
self.sections[sec_idx].subsections[sub_idx].collapsed = !self.sections[sec_idx].subsections[sub_idx].collapsed;
|
||||||
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,12 +357,15 @@ impl App {
|
|||||||
fn open_fold(&mut self) {
|
fn open_fold(&mut self) {
|
||||||
if let Some(idx) = self.state.selected() {
|
if let Some(idx) = self.state.selected() {
|
||||||
match self.flattened_items[idx] {
|
match self.flattened_items[idx] {
|
||||||
FlattenedItem::Header(sec_idx) => {
|
FlattenedItem::SectionHeader(sec_idx) => {
|
||||||
self.sections[sec_idx].collapsed = false;
|
self.sections[sec_idx].collapsed = false;
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
FlattenedItem::Option(_sec_idx, _) => {
|
FlattenedItem::SubSectionHeader(sec_idx, sub_idx) => {
|
||||||
|
self.sections[sec_idx].subsections[sub_idx].collapsed = false;
|
||||||
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -327,15 +373,24 @@ impl App {
|
|||||||
fn close_fold(&mut self) {
|
fn close_fold(&mut self) {
|
||||||
if let Some(idx) = self.state.selected() {
|
if let Some(idx) = self.state.selected() {
|
||||||
match self.flattened_items[idx] {
|
match self.flattened_items[idx] {
|
||||||
FlattenedItem::Header(sec_idx) => {
|
FlattenedItem::SectionHeader(sec_idx) => {
|
||||||
self.sections[sec_idx].collapsed = true;
|
self.sections[sec_idx].collapsed = true;
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
FlattenedItem::Option(sec_idx, _) => {
|
FlattenedItem::SectionOption(sec_idx, _) => {
|
||||||
self.sections[sec_idx].collapsed = true;
|
self.sections[sec_idx].collapsed = true;
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
self.state.select(Some(self.find_header_index(sec_idx)));
|
self.state.select(Some(self.find_header_index(sec_idx)));
|
||||||
}
|
}
|
||||||
|
FlattenedItem::SubSectionHeader(sec_idx, sub_idx) => {
|
||||||
|
self.sections[sec_idx].subsections[sub_idx].collapsed = true;
|
||||||
|
self.update_flattened();
|
||||||
|
}
|
||||||
|
FlattenedItem::SubSectionOption(sec_idx, sub_idx, _) => {
|
||||||
|
self.sections[sec_idx].subsections[sub_idx].collapsed = true;
|
||||||
|
self.update_flattened();
|
||||||
|
self.state.select(Some(self.find_subsection_header_index(sec_idx, sub_idx)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -343,6 +398,9 @@ impl App {
|
|||||||
fn open_all(&mut self) {
|
fn open_all(&mut self) {
|
||||||
for sec in &mut self.sections {
|
for sec in &mut self.sections {
|
||||||
sec.collapsed = false;
|
sec.collapsed = false;
|
||||||
|
for sub in &mut sec.subsections {
|
||||||
|
sub.collapsed = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
}
|
}
|
||||||
@@ -350,62 +408,116 @@ impl App {
|
|||||||
fn close_all(&mut self) {
|
fn close_all(&mut self) {
|
||||||
for sec in &mut self.sections {
|
for sec in &mut self.sections {
|
||||||
sec.collapsed = true;
|
sec.collapsed = true;
|
||||||
|
for sub in &mut sec.subsections {
|
||||||
|
sub.collapsed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.update_flattened();
|
self.update_flattened();
|
||||||
self.jump_top();
|
self.jump_top();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_header_index(&self, sec_idx: usize) -> usize {
|
fn find_header_index(&self, sec_idx: usize) -> usize {
|
||||||
self.flattened_items.iter().position(|item| matches!(item, FlattenedItem::Header(s) if *s == sec_idx)).unwrap_or(0)
|
self.flattened_items.iter().position(|item| matches!(item, FlattenedItem::SectionHeader(s) if *s == sec_idx)).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_subsection_header_index(&self, sec_idx: usize, sub_idx: usize) -> usize {
|
||||||
|
self.flattened_items.iter().position(|item| matches!(item, FlattenedItem::SubSectionHeader(s, sub) if *s == sec_idx && *sub == sub_idx)).unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn perform_search(&mut self) {
|
fn find_next_match(&mut self, direction: SearchDirection) {
|
||||||
let query = self.input_buffer.to_lowercase();
|
let query = self.last_search_query.to_lowercase();
|
||||||
if query.is_empty() { return; }
|
if query.is_empty() { return; }
|
||||||
|
|
||||||
let mut search_hits: Vec<(usize, usize, String)> = Vec::new();
|
let mut search_hits: Vec<(usize, Option<usize>, usize, String)> = Vec::new();
|
||||||
for (s_idx, section) in self.sections.iter().enumerate() {
|
for (s_idx, section) in self.sections.iter().enumerate() {
|
||||||
for (o_idx, opt) in section.options.iter().enumerate() {
|
for (o_idx, opt) in section.root_options.iter().enumerate() {
|
||||||
search_hits.push((s_idx, o_idx, opt.raw.name.to_lowercase()));
|
search_hits.push((s_idx, None, o_idx, opt.raw.name.to_lowercase()));
|
||||||
|
}
|
||||||
|
for (sub_idx, subsection) in section.subsections.iter().enumerate() {
|
||||||
|
for (o_idx, opt) in subsection.options.iter().enumerate() {
|
||||||
|
search_hits.push((s_idx, Some(sub_idx), o_idx, opt.raw.name.to_lowercase()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_flat_idx = self.state.selected().unwrap_or(0);
|
let current_flat_idx = self.state.selected().unwrap_or(0);
|
||||||
let current_linear_pos = if let Some(item) = self.flattened_items.get(current_flat_idx) {
|
let current_hit_index = if let Some(item) = self.flattened_items.get(current_flat_idx) {
|
||||||
match item {
|
match item {
|
||||||
FlattenedItem::Option(curr_s, curr_o) => {
|
FlattenedItem::SectionHeader(s) => {
|
||||||
search_hits.iter().position(|(s, o, _)| s == curr_s && o == curr_o).unwrap_or(0)
|
search_hits.iter().position(|(s_idx, _, _, _)| *s_idx > *s).unwrap_or(0)
|
||||||
},
|
}
|
||||||
FlattenedItem::Header(curr_s) => {
|
FlattenedItem::SectionOption(s, o) => {
|
||||||
search_hits.iter().position(|(s, _, _)| s >= curr_s).unwrap_or(0)
|
search_hits.iter().position(|(s_idx, sub, o_idx, _)| *s_idx == *s && sub.is_none() && *o_idx == *o).unwrap_or(0)
|
||||||
}
|
}
|
||||||
}
|
FlattenedItem::SubSectionHeader(s, sub) => {
|
||||||
|
search_hits.iter().position(|(s_idx, sub_idx, _, _)| *s_idx == *s && *sub_idx == Some(*sub)).unwrap_or(0)
|
||||||
|
}
|
||||||
|
FlattenedItem::SubSectionOption(s, sub, o) => {
|
||||||
|
search_hits.iter().position(|(s_idx, sub_idx, o_idx, _)| *s_idx == *s && *sub_idx == Some(*sub) && *o_idx == *o).unwrap_or(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
||||||
let match_res = search_hits.iter()
|
let match_res = match direction {
|
||||||
.skip(current_linear_pos + 1)
|
SearchDirection::Forward => {
|
||||||
.find(|(_, _, name)| name.contains(&query))
|
|
||||||
.or_else(|| {
|
|
||||||
search_hits.iter()
|
search_hits.iter()
|
||||||
.take(current_linear_pos + 1)
|
.skip(current_hit_index + 1)
|
||||||
.find(|(_, _, name)| name.contains(&query))
|
.find(|(_, _, _, name)| name.contains(&query))
|
||||||
});
|
.or_else(|| {
|
||||||
|
search_hits.iter()
|
||||||
|
.take(current_hit_index + 1)
|
||||||
|
.find(|(_, _, _, name)| name.contains(&query))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
SearchDirection::Backward => {
|
||||||
|
search_hits.iter()
|
||||||
|
.take(current_hit_index)
|
||||||
|
.rev()
|
||||||
|
.find(|(_, _, _, name)| name.contains(&query))
|
||||||
|
.or_else(|| {
|
||||||
|
search_hits.iter()
|
||||||
|
.skip(current_hit_index)
|
||||||
|
.rev()
|
||||||
|
.find(|(_, _, _, name)| name.contains(&query))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if let Some((s_idx, o_idx, _)) = match_res {
|
if let Some((s_idx, sub_idx, o_idx, _)) = match_res {
|
||||||
let s_idx = *s_idx;
|
let s_idx = *s_idx;
|
||||||
let o_idx = *o_idx;
|
let o_idx = *o_idx;
|
||||||
let opt_name = self.sections[s_idx].options[o_idx].raw.name.clone();
|
|
||||||
|
let opt_name = if let Some(sub) = sub_idx {
|
||||||
|
self.sections[s_idx].subsections[*sub].options[o_idx].raw.name.clone()
|
||||||
|
} else {
|
||||||
|
self.sections[s_idx].root_options[o_idx].raw.name.clone()
|
||||||
|
};
|
||||||
|
|
||||||
if self.sections[s_idx].collapsed {
|
if self.sections[s_idx].collapsed {
|
||||||
self.sections[s_idx].collapsed = false;
|
self.sections[s_idx].collapsed = false;
|
||||||
self.update_flattened();
|
|
||||||
}
|
}
|
||||||
|
if let Some(sub) = sub_idx {
|
||||||
|
if self.sections[s_idx].subsections[*sub].collapsed {
|
||||||
|
self.sections[s_idx].subsections[*sub].collapsed = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.update_flattened();
|
||||||
|
|
||||||
|
let target_item = if let Some(sub) = sub_idx {
|
||||||
|
FlattenedItem::SubSectionOption(s_idx, *sub, o_idx)
|
||||||
|
} else {
|
||||||
|
FlattenedItem::SectionOption(s_idx, o_idx)
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| {
|
if let Some(idx) = self.flattened_items.iter().position(|item| {
|
||||||
matches!(item, FlattenedItem::Option(s, o) if *s == s_idx && *o == o_idx)
|
match (item, target_item) {
|
||||||
|
(FlattenedItem::SectionOption(s1, o1), FlattenedItem::SectionOption(s2, o2)) => *s1 == s2 && *o1 == o2,
|
||||||
|
(FlattenedItem::SubSectionOption(s1, sub1, o1), FlattenedItem::SubSectionOption(s2, sub2, o2)) => *s1 == s2 && *sub1 == sub2 && *o1 == o2,
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
self.state.select(Some(idx));
|
self.state.select(Some(idx));
|
||||||
self.status_msg = format!("Found: '{}'", opt_name);
|
self.status_msg = format!("Found: '{}'", opt_name);
|
||||||
@@ -436,12 +548,40 @@ impl App {
|
|||||||
groups.entry(section).or_default().push(MesonOption::from_raw(raw));
|
groups.entry(section).or_default().push(MesonOption::from_raw(raw));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.sections = groups.into_iter().map(|(name, mut opts)| {
|
self.sections = groups.into_iter().map(|(name, opts)| {
|
||||||
opts.sort_by(|a, b| a.raw.name.cmp(&b.raw.name));
|
let mut root_options = Vec::new();
|
||||||
|
let mut subsections_map: HashMap<String, Vec<MesonOption>> = HashMap::new();
|
||||||
|
|
||||||
|
if name == "user" {
|
||||||
|
for opt in opts {
|
||||||
|
if let Some((prefix, _)) = opt.raw.name.split_once(':') {
|
||||||
|
subsections_map.entry(prefix.to_string()).or_default().push(opt);
|
||||||
|
} else {
|
||||||
|
root_options.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root_options = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
root_options.sort_by(|a, b| a.raw.name.cmp(&b.raw.name));
|
||||||
|
|
||||||
|
let mut subsections: Vec<SubSection> = subsections_map.into_iter().map(|(sub_name, mut sub_opts)| {
|
||||||
|
sub_opts.sort_by(|a, b| a.raw.name.cmp(&b.raw.name));
|
||||||
|
SubSection {
|
||||||
|
name: sub_name,
|
||||||
|
collapsed: true,
|
||||||
|
options: sub_opts,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
subsections.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
Section {
|
Section {
|
||||||
name,
|
name,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
options: opts
|
root_options,
|
||||||
|
subsections
|
||||||
}
|
}
|
||||||
}).collect();
|
}).collect();
|
||||||
|
|
||||||
@@ -461,7 +601,7 @@ impl App {
|
|||||||
self.state.select(Some(0));
|
self.state.select(Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_opts: usize = self.sections.iter().map(|s| s.options.len()).sum();
|
let total_opts: usize = self.sections.iter().map(|s| s.root_options.len() + s.subsections.iter().map(|sub| sub.options.len()).sum::<usize>()).sum();
|
||||||
self.status_msg = format!("Loaded {} options in {} sections.", total_opts, self.sections.len());
|
self.status_msg = format!("Loaded {} options in {} sections.", total_opts, self.sections.len());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -469,11 +609,18 @@ impl App {
|
|||||||
fn apply_changes(&mut self, show_overlay: bool) -> Result<bool> {
|
fn apply_changes(&mut self, show_overlay: bool) -> Result<bool> {
|
||||||
let mut dirty_opts = Vec::new();
|
let mut dirty_opts = Vec::new();
|
||||||
for section in &self.sections {
|
for section in &self.sections {
|
||||||
for opt in §ion.options {
|
for opt in §ion.root_options {
|
||||||
if opt.dirty {
|
if opt.dirty {
|
||||||
dirty_opts.push(opt);
|
dirty_opts.push(opt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for sub in §ion.subsections {
|
||||||
|
for opt in &sub.options {
|
||||||
|
if opt.dirty {
|
||||||
|
dirty_opts.push(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dirty_opts.is_empty() {
|
if dirty_opts.is_empty() {
|
||||||
@@ -512,25 +659,39 @@ impl App {
|
|||||||
|
|
||||||
if output.status.success() {
|
if output.status.success() {
|
||||||
for section in self.sections.iter_mut() {
|
for section in self.sections.iter_mut() {
|
||||||
for opt in section.options.iter_mut() {
|
for opt in section.root_options.iter_mut() {
|
||||||
if opt.dirty {
|
if opt.dirty {
|
||||||
opt.raw.value = opt.new_value.clone();
|
opt.raw.value = opt.new_value.clone();
|
||||||
opt.dirty = false;
|
opt.dirty = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for sub in section.subsections.iter_mut() {
|
||||||
|
for opt in sub.options.iter_mut() {
|
||||||
|
if opt.dirty {
|
||||||
|
opt.raw.value = opt.new_value.clone();
|
||||||
|
opt.dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.status_msg = "Configuration applied successfully.".to_string();
|
self.status_msg = "Configuration applied successfully.".to_string();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
self.status_msg = format!("Error applying config. check overlay.");
|
self.status_msg = "Error applying config. check overlay.".to_string();
|
||||||
Ok(false)
|
Ok(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_selected_option_mut(&mut self) -> Option<&mut MesonOption> {
|
fn get_selected_option_mut(&mut self) -> Option<&mut MesonOption> {
|
||||||
if let Some(idx) = self.state.selected() {
|
if let Some(idx) = self.state.selected() {
|
||||||
if let FlattenedItem::Option(sec_idx, opt_idx) = self.flattened_items[idx] {
|
match self.flattened_items[idx] {
|
||||||
return Some(&mut self.sections[sec_idx].options[opt_idx]);
|
FlattenedItem::SectionOption(sec_idx, opt_idx) => {
|
||||||
|
return Some(&mut self.sections[sec_idx].root_options[opt_idx]);
|
||||||
|
}
|
||||||
|
FlattenedItem::SubSectionOption(sec_idx, sub_idx, opt_idx) => {
|
||||||
|
return Some(&mut self.sections[sec_idx].subsections[sub_idx].options[opt_idx]);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -573,10 +734,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut App) ->
|
|||||||
loop {
|
loop {
|
||||||
terminal.draw(|f| ui(f, app))?;
|
terminal.draw(|f| ui(f, app))?;
|
||||||
|
|
||||||
if let Event::Key(key) = event::read()? {
|
match event::read()? {
|
||||||
if key.kind != KeyEventKind::Press { continue; }
|
Event::Key(key) => {
|
||||||
|
if key.kind != KeyEventKind::Press { continue; }
|
||||||
|
|
||||||
match app.input_mode {
|
match app.input_mode {
|
||||||
InputMode::OutputViewer => {
|
InputMode::OutputViewer => {
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
|
KeyCode::Esc | KeyCode::Enter | KeyCode::Char('q') => {
|
||||||
@@ -661,21 +823,54 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut App) ->
|
|||||||
KeyCode::Char('/') => {
|
KeyCode::Char('/') => {
|
||||||
app.pending_g = false;
|
app.pending_g = false;
|
||||||
app.pending_z = false;
|
app.pending_z = false;
|
||||||
|
app.search_direction = SearchDirection::Forward;
|
||||||
app.input_mode = InputMode::Searching;
|
app.input_mode = InputMode::Searching;
|
||||||
app.input_buffer.clear();
|
app.input_buffer.clear();
|
||||||
},
|
},
|
||||||
|
KeyCode::Char('?') => {
|
||||||
|
app.pending_g = false;
|
||||||
|
app.pending_z = false;
|
||||||
|
app.search_direction = SearchDirection::Backward;
|
||||||
|
app.input_mode = InputMode::Searching;
|
||||||
|
app.input_buffer.clear();
|
||||||
|
},
|
||||||
|
KeyCode::Char('n') => {
|
||||||
|
app.pending_g = false;
|
||||||
|
app.pending_z = false;
|
||||||
|
app.find_next_match(app.search_direction);
|
||||||
|
},
|
||||||
|
KeyCode::Char('N') => {
|
||||||
|
app.pending_g = false;
|
||||||
|
app.pending_z = false;
|
||||||
|
app.find_next_match(app.search_direction.opposite());
|
||||||
|
},
|
||||||
KeyCode::Enter | KeyCode::Char(' ') => {
|
KeyCode::Enter | KeyCode::Char(' ') => {
|
||||||
app.pending_g = false;
|
app.pending_g = false;
|
||||||
app.pending_z = false;
|
app.pending_z = false;
|
||||||
|
|
||||||
if let Some(idx) = app.state.selected() {
|
if let Some(idx) = app.state.selected() {
|
||||||
match app.flattened_items[idx] {
|
match app.flattened_items[idx] {
|
||||||
FlattenedItem::Header(sec_idx) => {
|
FlattenedItem::SectionHeader(sec_idx) => {
|
||||||
app.sections[sec_idx].collapsed = !app.sections[sec_idx].collapsed;
|
app.sections[sec_idx].collapsed = !app.sections[sec_idx].collapsed;
|
||||||
app.update_flattened();
|
app.update_flattened();
|
||||||
}
|
}
|
||||||
FlattenedItem::Option(sec_idx, opt_idx) => {
|
FlattenedItem::SubSectionHeader(sec_idx, sub_idx) => {
|
||||||
let opt = &mut app.sections[sec_idx].options[opt_idx];
|
app.sections[sec_idx].subsections[sub_idx].collapsed = !app.sections[sec_idx].subsections[sub_idx].collapsed;
|
||||||
|
app.update_flattened();
|
||||||
|
}
|
||||||
|
FlattenedItem::SectionOption(sec_idx, opt_idx) => {
|
||||||
|
let opt = &mut app.sections[sec_idx].root_options[opt_idx];
|
||||||
|
if !opt.toggle_or_cycle() {
|
||||||
|
app.input_mode = InputMode::Editing;
|
||||||
|
app.input_buffer = match &opt.new_value {
|
||||||
|
serde_json::Value::Number(n) => n.to_string(),
|
||||||
|
serde_json::Value::String(s) => s.clone(),
|
||||||
|
_ => "".to_string(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlattenedItem::SubSectionOption(sec_idx, sub_idx, opt_idx) => {
|
||||||
|
let opt = &mut app.sections[sec_idx].subsections[sub_idx].options[opt_idx];
|
||||||
if !opt.toggle_or_cycle() {
|
if !opt.toggle_or_cycle() {
|
||||||
app.input_mode = InputMode::Editing;
|
app.input_mode = InputMode::Editing;
|
||||||
app.input_buffer = match &opt.new_value {
|
app.input_buffer = match &opt.new_value {
|
||||||
@@ -759,7 +954,8 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut App) ->
|
|||||||
},
|
},
|
||||||
InputMode::Searching => match key.code {
|
InputMode::Searching => match key.code {
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
app.perform_search();
|
app.last_search_query = app.input_buffer.clone();
|
||||||
|
app.find_next_match(app.search_direction);
|
||||||
app.input_mode = InputMode::Normal;
|
app.input_mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
@@ -775,6 +971,25 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>, app: &mut App) ->
|
|||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Event::Mouse(mouse) => match mouse.kind {
|
||||||
|
event::MouseEventKind::ScrollDown => match app.input_mode {
|
||||||
|
InputMode::Normal => app.next(),
|
||||||
|
InputMode::OutputViewer => {
|
||||||
|
app.output_scroll = app.output_scroll.saturating_add(1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
event::MouseEventKind::ScrollUp => match app.input_mode {
|
||||||
|
InputMode::Normal => app.previous(),
|
||||||
|
InputMode::OutputViewer => {
|
||||||
|
app.output_scroll = app.output_scroll.saturating_sub(1);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -806,8 +1021,10 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
f.render_widget(title, chunks[0]);
|
f.render_widget(title, chunks[0]);
|
||||||
|
|
||||||
let max_name_width = app.sections.iter()
|
let max_name_width = app.sections.iter()
|
||||||
.flat_map(|s| s.options.iter())
|
.flat_map(|s| {
|
||||||
.map(|o| o.raw.name.len())
|
s.root_options.iter().map(|o| o.raw.name.len())
|
||||||
|
.chain(s.subsections.iter().flat_map(|sub| sub.options.iter().map(|o| o.raw.name.len())))
|
||||||
|
})
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(20)
|
.unwrap_or(20)
|
||||||
.max(20);
|
.max(20);
|
||||||
@@ -819,15 +1036,15 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|item| {
|
.map(|item| {
|
||||||
match item {
|
match item {
|
||||||
FlattenedItem::Header(sec_idx) => {
|
FlattenedItem::SectionHeader(sec_idx) => {
|
||||||
let sec = &app.sections[*sec_idx];
|
let sec = &app.sections[*sec_idx];
|
||||||
let icon = if sec.collapsed { "▶" } else { "▼" };
|
let icon = if sec.collapsed { "▶" } else { "▼" };
|
||||||
let count = sec.options.len();
|
let count = sec.root_options.len() + sec.subsections.iter().map(|s| s.options.len()).sum::<usize>();
|
||||||
let content = format!("{} {} ({})", icon, sec.name.to_uppercase(), count);
|
let content = format!("{} {} ({})", icon, sec.name.to_uppercase(), count);
|
||||||
ListItem::new(content).style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD))
|
ListItem::new(content).style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD))
|
||||||
},
|
},
|
||||||
FlattenedItem::Option(sec_idx, opt_idx) => {
|
FlattenedItem::SectionOption(sec_idx, opt_idx) => {
|
||||||
let opt = &app.sections[*sec_idx].options[*opt_idx];
|
let opt = &app.sections[*sec_idx].root_options[*opt_idx];
|
||||||
let val_str = opt.format_value();
|
let val_str = opt.format_value();
|
||||||
let dirty_str = if opt.dirty { "*" } else { " " };
|
let dirty_str = if opt.dirty { "*" } else { " " };
|
||||||
let choices_str = opt.format_choices();
|
let choices_str = opt.format_choices();
|
||||||
@@ -840,7 +1057,44 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let p1 = Span::styled(
|
let p1 = Span::styled(
|
||||||
format!(" {} {:<w$} : ", dirty_str, opt.raw.name, w=max_name_width),
|
format!(" {} {:<w$} : ", dirty_str, opt.raw.name, w=max_name_width),
|
||||||
|
Style::default().fg(name_color)
|
||||||
|
);
|
||||||
|
|
||||||
|
let p2 = Span::styled(
|
||||||
|
format!("{:<w$} ", val_str, w=val_column_width),
|
||||||
|
Style::default().fg(val_color)
|
||||||
|
);
|
||||||
|
|
||||||
|
let p3 = Span::styled(
|
||||||
|
choices_str,
|
||||||
|
Style::default().fg(Color::LightMagenta)
|
||||||
|
);
|
||||||
|
|
||||||
|
ListItem::new(Line::from(vec![p1, p2, p3]))
|
||||||
|
},
|
||||||
|
FlattenedItem::SubSectionHeader(sec_idx, sub_idx) => {
|
||||||
|
let sub = &app.sections[*sec_idx].subsections[*sub_idx];
|
||||||
|
let icon = if sub.collapsed { "▶" } else { "▼" };
|
||||||
|
let count = sub.options.len();
|
||||||
|
let content = format!(" {} {} ({})", icon, sub.name, count);
|
||||||
|
ListItem::new(content).style(Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD))
|
||||||
|
},
|
||||||
|
FlattenedItem::SubSectionOption(sec_idx, sub_idx, opt_idx) => {
|
||||||
|
let opt = &app.sections[*sec_idx].subsections[*sub_idx].options[*opt_idx];
|
||||||
|
let val_str = opt.format_value();
|
||||||
|
let dirty_str = if opt.dirty { "*" } else { " " };
|
||||||
|
let choices_str = opt.format_choices();
|
||||||
|
|
||||||
|
let name_color = if opt.dirty { Color::Yellow } else { Color::White };
|
||||||
|
let val_color = match val_str.as_str() {
|
||||||
|
"enabled" => Color::Green,
|
||||||
|
"disabled" => Color::Red,
|
||||||
|
_ => Color::Cyan,
|
||||||
|
};
|
||||||
|
|
||||||
|
let p1 = Span::styled(
|
||||||
|
format!(" {} {:<w$} : ", dirty_str, opt.raw.name, w=max_name_width),
|
||||||
Style::default().fg(name_color)
|
Style::default().fg(name_color)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -871,8 +1125,14 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
InputMode::Normal => {
|
InputMode::Normal => {
|
||||||
let mut desc = String::new();
|
let mut desc = String::new();
|
||||||
if let Some(idx) = app.state.selected() {
|
if let Some(idx) = app.state.selected() {
|
||||||
if let FlattenedItem::Option(s, o) = app.flattened_items[idx] {
|
match app.flattened_items[idx] {
|
||||||
desc = app.sections[s].options[o].raw.description.clone().unwrap_or_default();
|
FlattenedItem::SectionOption(s, o) => {
|
||||||
|
desc = app.sections[s].root_options[o].raw.description.clone().unwrap_or_default();
|
||||||
|
}
|
||||||
|
FlattenedItem::SubSectionOption(s, sub, o) => {
|
||||||
|
desc = app.sections[s].subsections[sub].options[o].raw.description.clone().unwrap_or_default();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -891,7 +1151,8 @@ fn ui(f: &mut Frame, app: &mut App) {
|
|||||||
f.render_widget(p, chunks[2]);
|
f.render_widget(p, chunks[2]);
|
||||||
}
|
}
|
||||||
InputMode::Searching => {
|
InputMode::Searching => {
|
||||||
let p = Paragraph::new(format!("/{}", app.input_buffer))
|
let prompt = if app.search_direction == SearchDirection::Forward { "/" } else { "?" };
|
||||||
|
let p = Paragraph::new(format!("{}{}", prompt, app.input_buffer))
|
||||||
.style(Style::default().fg(Color::LightBlue))
|
.style(Style::default().fg(Color::LightBlue))
|
||||||
.block(Block::default().borders(Borders::ALL).title("Search (Enter to jump)"));
|
.block(Block::default().borders(Borders::ALL).title("Search (Enter to jump)"));
|
||||||
f.render_widget(p, chunks[2]);
|
f.render_widget(p, chunks[2]);
|
||||||
|
|||||||
Reference in New Issue
Block a user