Add basic command palette
This commit is contained in:
60
src/app.rs
60
src/app.rs
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
sync::{Arc, mpsc},
|
||||
thread::JoinHandle,
|
||||
@@ -8,11 +7,12 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
command_palette::{self, CommandPalette},
|
||||
file_editor::{FileEditor, SaveStatus},
|
||||
folder::Folder,
|
||||
preferences::Preferences,
|
||||
text_styles::{H1, H1_MONO, H2, H2_MONO, H3, H3_MONO, H4, H4_MONO, H5, H5_MONO, H6, H6_MONO},
|
||||
util::{GuiSender, file_mtime, log_error},
|
||||
util::{GuiSender, log_error},
|
||||
};
|
||||
use egui::{
|
||||
Align, Button, Context, FontData, FontDefinitions, FontFamily, FontId, Frame, Image, Key,
|
||||
@@ -40,6 +40,9 @@ pub struct App {
|
||||
open_tab_index: Option<usize>,
|
||||
|
||||
next_tab_id: TabId,
|
||||
|
||||
#[serde(skip)]
|
||||
command_palette: Option<CommandPalette>,
|
||||
}
|
||||
|
||||
pub struct Jobs {
|
||||
@@ -129,6 +132,7 @@ impl Default for App {
|
||||
next_tab_id: 2,
|
||||
show_folders: false,
|
||||
folders: vec![],
|
||||
command_palette: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,6 +296,10 @@ impl eframe::App for App {
|
||||
if input.consume_key(Modifiers::CTRL, Key::S) {
|
||||
self.save_active_tab(ctx);
|
||||
}
|
||||
|
||||
if input.consume_key(Modifiers::CTRL, Key::K) {
|
||||
self.command_palette = Some(Default::default());
|
||||
}
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
@@ -310,19 +318,10 @@ impl eframe::App for App {
|
||||
self.jobs.start(ui.ctx(), move || {
|
||||
let file_path = rfd::FileDialog::new().pick_file()?;
|
||||
|
||||
let mut file = fs::File::open(&file_path)
|
||||
.inspect_err(|e| log::error!("Failed to open {file_path:?}: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?;
|
||||
|
||||
let mut text = String::new();
|
||||
file.read_to_string(&mut text)
|
||||
.inspect_err(|e| log::error!("Failed to read {file_path:?}: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let editor = FileEditor::from_file(file_path, &text, mtime);
|
||||
Some(Action::OpenFile(editor))
|
||||
log_error(eyre!("Failed to open file {file_path:?}"), || {
|
||||
FileEditor::open_file(file_path)
|
||||
})
|
||||
.map(Action::OpenFile)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -445,23 +444,10 @@ impl eframe::App for App {
|
||||
if let Some(file_path) = response.open_file {
|
||||
let file_path = file_path.to_owned();
|
||||
self.jobs.start(ui.ctx(), move || {
|
||||
let mut file = fs::File::open(&file_path)
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to open {file_path:?}: {e}")
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let mtime = log_error(eyre!("file_path:?"), || file_mtime(&file))?;
|
||||
|
||||
let mut text = String::new();
|
||||
file.read_to_string(&mut text)
|
||||
.inspect_err(|e| {
|
||||
log::error!("Failed to read {file_path:?}: {e}")
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let editor = FileEditor::from_file(file_path, &text, mtime);
|
||||
Some(Action::OpenFile(editor))
|
||||
log_error(eyre!("Failed to open file {file_path:?}"), || {
|
||||
FileEditor::open_file(file_path)
|
||||
})
|
||||
.map(Action::OpenFile)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -489,6 +475,16 @@ impl eframe::App for App {
|
||||
egui::warn_if_debug_build(ui);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(command_palette) = &mut self.command_palette {
|
||||
match command_palette.show(ctx, &mut self.jobs, &mut self.folders) {
|
||||
command_palette::Response::None => {}
|
||||
command_palette::Response::Close => {
|
||||
self.command_palette = None;
|
||||
ctx.request_repaint();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
146
src/command_palette.rs
Normal file
146
src/command_palette.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use std::{
|
||||
cmp::min,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use egui::{Key, Modifiers, RichText, Ui, Window};
|
||||
use eyre::eyre;
|
||||
|
||||
use crate::{
|
||||
app::{Action, Jobs},
|
||||
file_editor::FileEditor,
|
||||
folder::Folder,
|
||||
util::log_error,
|
||||
};
|
||||
|
||||
pub struct CommandPalette {
|
||||
first_frame: bool,
|
||||
input: String,
|
||||
results: Vec<OpenFileCommand>,
|
||||
list_i: usize,
|
||||
}
|
||||
|
||||
impl Default for CommandPalette {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
first_frame: true,
|
||||
input: Default::default(),
|
||||
results: Default::default(),
|
||||
list_i: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OpenFileCommand {
|
||||
pretty: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
pub enum Response {
|
||||
None,
|
||||
Close,
|
||||
}
|
||||
|
||||
impl CommandPalette {
|
||||
pub fn show(
|
||||
&mut self,
|
||||
ctx: &egui::Context,
|
||||
jobs: &mut Jobs,
|
||||
folders: &mut [Folder],
|
||||
) -> Response {
|
||||
Window::new("Command Palette")
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
let text_edit = ui.text_edit_singleline(&mut self.input);
|
||||
let mut response = Response::None;
|
||||
if self.first_frame {
|
||||
self.first_frame = false;
|
||||
text_edit.request_focus();
|
||||
}
|
||||
|
||||
if text_edit.has_focus() || text_edit.lost_focus() {
|
||||
ui.input_mut(|input| {
|
||||
if input.consume_key(Modifiers::NONE, Key::ArrowUp) {
|
||||
self.list_i = self.list_i.saturating_sub(1);
|
||||
};
|
||||
if input.consume_key(Modifiers::NONE, Key::ArrowDown) {
|
||||
self.list_i =
|
||||
min(self.list_i + 1, self.results.len().saturating_sub(1));
|
||||
};
|
||||
if input.consume_key(Modifiers::NONE, Key::Enter)
|
||||
&& let Some(result) = self.results.get(self.list_i)
|
||||
{
|
||||
let path = result.path.clone();
|
||||
log::info!("open {}", result.pretty);
|
||||
jobs.start(ui.ctx(), move || {
|
||||
log_error(eyre!("Failed to open file {path:?}"), move || {
|
||||
FileEditor::open_file(path)
|
||||
})
|
||||
.map(Action::OpenFile)
|
||||
});
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if !text_edit.has_focus() {
|
||||
log::info!("sad face");
|
||||
response = Response::Close;
|
||||
}
|
||||
|
||||
if text_edit.changed() {
|
||||
self.results.clear();
|
||||
|
||||
if !self.input.is_empty() {
|
||||
self.input = self.input.to_lowercase();
|
||||
search_files(ui, &self.input, "".as_ref(), folders, &mut self.results);
|
||||
}
|
||||
|
||||
self.list_i = min(self.list_i, self.results.len().saturating_sub(1));
|
||||
}
|
||||
|
||||
for (i, result) in self.results.iter().enumerate() {
|
||||
let highlight = i == self.list_i;
|
||||
ui.horizontal(|ui| {
|
||||
let mut label = RichText::new("> open");
|
||||
if highlight {
|
||||
label = label.strong().underline();
|
||||
}
|
||||
ui.label(label);
|
||||
ui.monospace(&result.pretty);
|
||||
});
|
||||
}
|
||||
|
||||
response
|
||||
})
|
||||
.expect("Window is always open")
|
||||
.inner
|
||||
.expect("Windows is never collapsed")
|
||||
}
|
||||
}
|
||||
|
||||
/// Search the tree of [Folder]s for files matching `query`.
|
||||
fn search_files(
|
||||
ui: &mut Ui,
|
||||
query: &str,
|
||||
path: &Path,
|
||||
folders: &mut [Folder],
|
||||
out: &mut Vec<OpenFileCommand>,
|
||||
) {
|
||||
for folder in folders {
|
||||
let Some(folder) = folder.load(ui) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let path = path.join(&folder.name);
|
||||
|
||||
for file in &folder.child_files {
|
||||
if file.name.to_lowercase().contains(query) {
|
||||
let pretty = path.join(&file.name).to_string_lossy().to_string();
|
||||
let path = file.path.clone();
|
||||
out.push(OpenFileCommand { pretty, path });
|
||||
}
|
||||
}
|
||||
|
||||
search_files(ui, query, &path, &mut folder.child_folders, out);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{self, Display},
|
||||
fs::{self, File},
|
||||
io::Write,
|
||||
io::{Read as _, Write},
|
||||
ops::{Div as _, Sub as _},
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
@@ -13,7 +13,7 @@ use chrono::{DateTime, Local};
|
||||
use egui::{
|
||||
Align, Button, Context, DragAndDrop, Frame, Layout, ScrollArea, Ui, UiBuilder, Vec2, vec2,
|
||||
};
|
||||
use eyre::eyre;
|
||||
use eyre::{Context as _, eyre};
|
||||
use notify::{EventKind, Watcher};
|
||||
|
||||
use crate::{
|
||||
@@ -100,7 +100,7 @@ impl FileEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_file(file_path: PathBuf, contents: &str, mtime: DateTime<Local>) -> Self {
|
||||
pub fn from_path(file_path: PathBuf, contents: &str, mtime: DateTime<Local>) -> Self {
|
||||
let file_title = file_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().to_string())
|
||||
@@ -115,6 +115,19 @@ impl FileEditor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_file(file_path: PathBuf) -> eyre::Result<Self> {
|
||||
let mut file =
|
||||
fs::File::open(&file_path).wrap_err_with(|| eyre!("Failed to open {file_path:?}"))?;
|
||||
|
||||
let mtime = file_mtime(&file)?;
|
||||
|
||||
let mut text = String::new();
|
||||
file.read_to_string(&mut text)
|
||||
.wrap_err_with(|| eyre!("Failed to read {file_path:?}"))?;
|
||||
|
||||
Ok(FileEditor::from_path(file_path, &text, mtime))
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ impl LoadedFolder {
|
||||
}
|
||||
|
||||
impl Folder {
|
||||
fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
||||
pub fn load(&mut self, ui: &mut Ui) -> Option<&mut LoadedFolder> {
|
||||
if let Folder::NotLoaded { name, path } = self {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod app;
|
||||
pub mod command_palette;
|
||||
pub mod constants;
|
||||
pub mod custom_code_block;
|
||||
pub mod file_editor;
|
||||
|
||||
Reference in New Issue
Block a user