Skip to content

Commit 23b2d94

Browse files
author
Andrew Whaley
committed
Improved file manager
1 parent e691e4f commit 23b2d94

7 files changed

Lines changed: 119 additions & 32 deletions

File tree

.github/workflows/release.yml

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,31 @@ jobs:
2323
- name: Install cargo-espflash
2424
run: cargo install cargo-espflash --locked
2525

26-
- name: Build firmware image (app)
26+
- name: Cache ESP-IDF
27+
uses: actions/cache@v4
28+
with:
29+
path: |
30+
~/.espressif
31+
~/esp/esp-idf
32+
key: esp-idf-v5.5.2-${{ runner.os }}
33+
34+
- name: Install ESP-IDF Toolchain
2735
run: |
36+
sudo apt-get update
37+
sudo apt-get install -y git python3 python3-venv python3-pip cmake ninja-build
38+
mkdir -p $HOME/esp
39+
if [ ! -d "$HOME/esp/esp-idf/.git" ]; then
40+
git clone --depth 1 -b v5.5.2 https://github.com/espressif/esp-idf.git $HOME/esp/esp-idf
41+
fi
42+
if [ ! -d "$HOME/.espressif" ]; then
43+
$HOME/esp/esp-idf/install.sh esp32c3
44+
fi
45+
46+
- name: Build firmware image (app + full)
47+
run: |
48+
source $HOME/esp/esp-idf/export.sh
2849
cargo espflash save-image --release --chip=esp32c3 --target=riscv32imc-unknown-none-elf --package=tern-x4 firmware.bin
2950
cp firmware.bin tern-fw-${GITHUB_REF_NAME}.bin
30-
31-
- name: Build firmware image (full)
32-
run: |
3351
cargo espflash save-image --merge --release --chip=esp32c3 --target=riscv32imc-unknown-none-elf --package=tern-x4 ternfull-${GITHUB_REF_NAME}.bin
3452
3553
- name: Upload firmware artifact (app)

core/src/app/home.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ use crate::ui::{flush_queue, ListItem, ListView, Rect, RenderQueue, UiContext, V
1919
const START_MENU_MARGIN: i32 = 16;
2020
const START_MENU_RECENT_THUMB: i32 = 74;
2121
const START_MENU_ACTION_GAP: i32 = 12;
22-
const HEADER_Y: i32 = 24;
23-
const LIST_TOP: i32 = 60;
24-
const LINE_HEIGHT: i32 = 24;
25-
const LIST_MARGIN_X: i32 = 16;
22+
const HEADER_Y: i32 = 28;
23+
const LIST_TOP: i32 = 72;
24+
const LINE_HEIGHT: i32 = 30;
25+
const LIST_MARGIN_X: i32 = 18;
2626

2727
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2828
pub enum StartMenuSection {
@@ -143,9 +143,9 @@ impl HomeState {
143143

144144
pub fn menu_title(&self) -> String {
145145
if self.path.is_empty() {
146-
"Images".to_string()
146+
"/".to_string()
147147
} else {
148-
let mut title = String::from("Images/");
148+
let mut title = String::from("/");
149149
title.push_str(&self.path.join("/"));
150150
title
151151
}
@@ -575,7 +575,7 @@ impl HomeState {
575575
let mut list = ListView::new(&items);
576576
list.title = Some(title.as_str());
577577
list.footer = Some("Up/Down: select Confirm: open Back: up");
578-
list.empty_label = Some("No entries found in /images");
578+
list.empty_label = Some("No files found.");
579579
list.selected = self.selected;
580580
list.margin_x = LIST_MARGIN_X;
581581
list.header_y = HEADER_Y;

core/src/application.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ impl<'a, S: AppSource> Application<'a, S> {
432432
let action = match self.home.open_selected() {
433433
Ok(action) => action,
434434
Err(HomeOpenError::Empty) => {
435-
self.error_message = Some("No entries found in /images.".into());
435+
self.error_message = Some("No entries found.".into());
436436
self.state = AppState::Error;
437437
self.dirty = true;
438438
return;
@@ -547,7 +547,7 @@ impl<'a, S: AppSource> Application<'a, S> {
547547

548548
fn set_error(&mut self, err: ImageError) {
549549
let message = match err {
550-
ImageError::Io => "I/O error while accessing /images.".into(),
550+
ImageError::Io => "I/O error while accessing storage.".into(),
551551
ImageError::Decode => "Failed to decode image.".into(),
552552
ImageError::Unsupported => "Unsupported image format.".into(),
553553
ImageError::Message(message) => message,

x4/fatfs/ffconf.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@
113113
*/
114114

115115

116-
#define FF_USE_LFN 0
117-
#define FF_MAX_LFN 64
116+
#define FF_USE_LFN 1
117+
#define FF_MAX_LFN 255
118118
/* The FF_USE_LFN switches the support for LFN (long file name).
119119
/
120120
/ 0: Disable LFN. FF_MAX_LFN has no effect.
@@ -133,7 +133,7 @@
133133
/ ff_memfree() exemplified in ffsystem.c, need to be added to the project. */
134134

135135

136-
#define FF_LFN_UNICODE 0
136+
#define FF_LFN_UNICODE 2
137137
/* This option switches the character encoding on the API when LFN is enabled.
138138
/
139139
/ 0: ANSI/OEM in current CP (TCHAR = char)

x4/src/image_source.rs

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ pub trait UsbStorage {
4646
self.usb_write(path, offset, data)
4747
}
4848
fn usb_delete(&mut self, path: &str) -> Result<(), ImageError>;
49+
fn usb_rmdir(&mut self, path: &str) -> Result<(), ImageError>;
4950
fn usb_rename(&mut self, from: &str, to: &str) -> Result<(), ImageError>;
5051
fn usb_mkdir(&mut self, path: &str) -> Result<(), ImageError>;
5152
}
@@ -70,6 +71,13 @@ impl<F> SdImageSource<F>
7071
where
7172
F: Filesystem + 'static,
7273
{
74+
fn join_usb_path(dir: &str, name: &str) -> String {
75+
if dir.is_empty() || dir == "/" {
76+
return format!("/{}", name);
77+
}
78+
format!("{}/{}", dir.trim_end_matches('/'), name)
79+
}
80+
7381
fn build_path(path: &[String], name: &str) -> String {
7482
if path.is_empty() {
7583
return name.to_string();
@@ -253,8 +261,10 @@ where
253261
for<'a> F::File<'a>: 'static,
254262
{
255263
fn usb_list(&mut self, path: &str) -> Result<Vec<UsbDirEntry>, ImageError> {
256-
let dir = self.fs.open_directory(path).map_err(|_| ImageError::Io)?;
257-
let listed = dir.list().map_err(|_| ImageError::Io)?;
264+
let listed = {
265+
let dir = self.fs.open_directory(path).map_err(|_| ImageError::Io)?;
266+
dir.list().map_err(|_| ImageError::Io)?
267+
};
258268
let mut out = Vec::new();
259269
for entry in listed {
260270
out.push(UsbDirEntry {
@@ -361,6 +371,12 @@ where
361371
Ok(())
362372
}
363373

374+
fn usb_rmdir(&mut self, path: &str) -> Result<(), ImageError> {
375+
self.usb_delete_dir_recursive(path)?;
376+
self.fs.delete_file(path).map_err(|_| ImageError::Io)?;
377+
Ok(())
378+
}
379+
364380
fn usb_rename(&mut self, from: &str, to: &str) -> Result<(), ImageError> {
365381
self.fs.rename_file(from, to).map_err(|_| ImageError::Io)
366382
}
@@ -428,6 +444,34 @@ where
428444
.fs
429445
.delete_file(&format!("{}/{}", cache_legacy, title));
430446
}
447+
448+
fn usb_delete_dir_recursive(&mut self, path: &str) -> Result<(), ImageError>
449+
where
450+
F: UsbFsOps,
451+
{
452+
let listed = {
453+
let dir = self.fs.open_directory(path).map_err(|_| ImageError::Io)?;
454+
dir.list().map_err(|_| ImageError::Io)?
455+
};
456+
let entries: Vec<(String, bool)> = listed
457+
.into_iter()
458+
.map(|entry| (entry.name().to_string(), entry.is_directory()))
459+
.collect();
460+
for (name, is_dir) in entries {
461+
if name == "." || name == ".." {
462+
continue;
463+
}
464+
let full_path = Self::join_usb_path(path, &name);
465+
if is_dir {
466+
self.usb_delete_dir_recursive(&full_path)?;
467+
let _ = self.fs.delete_file(&full_path);
468+
} else {
469+
let _ = self.fs.delete_file(&full_path);
470+
self.cleanup_deleted_path_with_usb(&full_path);
471+
}
472+
}
473+
Ok(())
474+
}
431475
}
432476

433477
fn read_exact<R: Read + ?Sized>(reader: &mut R, mut buf: &mut [u8]) -> Result<(), ImageError> {
@@ -640,18 +684,27 @@ where
640684
for entry in listed {
641685
let name = entry.name().to_string();
642686
let short = entry.short_name().to_string();
687+
let upper = name.to_ascii_uppercase();
688+
let short_upper = short.to_ascii_uppercase();
689+
let short_is_hidden = short.starts_with('.');
643690
if !name.is_empty() {
644691
self.short_names.push((name.clone(), short));
645692
}
646693
if name.is_empty()
647-
|| name == Self::resume_filename()
648-
|| name == Self::resume_filename_legacy()
649-
|| name == Self::book_positions_filename()
650-
|| name == Self::book_positions_filename_legacy()
651-
|| name == Self::recent_entries_filename()
652-
|| name == Self::recent_entries_filename_legacy()
653-
|| name == Self::thumbnails_dirname()
654-
|| name == Self::thumbnails_dirname_legacy()
694+
|| name.starts_with('.')
695+
|| short_is_hidden
696+
|| upper == Self::resume_filename()
697+
|| upper == Self::resume_filename_legacy().to_ascii_uppercase()
698+
|| upper == Self::book_positions_filename()
699+
|| upper == Self::book_positions_filename_legacy().to_ascii_uppercase()
700+
|| upper == Self::recent_entries_filename()
701+
|| upper == Self::recent_entries_filename_legacy().to_ascii_uppercase()
702+
|| upper == Self::thumbnails_dirname()
703+
|| upper == Self::thumbnails_dirname_legacy().to_ascii_uppercase()
704+
|| short_upper == Self::resume_filename()
705+
|| short_upper == Self::book_positions_filename()
706+
|| short_upper == Self::recent_entries_filename()
707+
|| short_upper == Self::thumbnails_dirname()
655708
{
656709
continue;
657710
}

x4/src/sdspi_fatfs.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,12 +343,12 @@ impl FatFs {
343343
}
344344
}
345345

346-
fn null_terminate(path: &str) -> [u8; 256] {
346+
fn null_terminate(path: &str) -> [u8; 512] {
347347
assert!(
348-
path.len() < 256,
348+
path.len() < 512,
349349
"Path too long for static null-terminated buffer"
350350
);
351-
let mut null_terminated_path = [0u8; 256];
351+
let mut null_terminated_path = [0u8; 512];
352352
null_terminated_path[..path.len()].copy_from_slice(path.as_bytes());
353353
null_terminated_path
354354
}

x4/src/usb_mode.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -695,9 +695,25 @@ pub async fn poll<S: UsbStorage>(
695695
}
696696
}
697697
x if x == Command::Rmdir as u8 => {
698-
usb.last_err = Some(ErrorCode::NotPermitted);
699-
let response = encode_error(frame.req_id, cmd, ErrorCode::NotPermitted, "rmdir not supported");
700-
let _ = Write::write_all(tx, &response).await;
698+
let mut cursor = 0usize;
699+
let Some(path) = read_path(&frame.payload, &mut cursor) else {
700+
usb.last_err = Some(ErrorCode::InvalidArgs);
701+
let response = encode_error(frame.req_id, cmd, ErrorCode::InvalidArgs, "bad path");
702+
let _ = Write::write_all(tx, &response).await;
703+
continue;
704+
};
705+
match storage.usb_rmdir(&path) {
706+
Ok(()) => {
707+
usb.last_err = None;
708+
let response = encode_ok(frame.req_id, cmd, &[]);
709+
let _ = Write::write_all(tx, &response).await;
710+
}
711+
Err(err) => {
712+
usb.last_err = Some(ErrorCode::Io);
713+
let response = encode_error_for(frame.req_id, cmd, ErrorCode::Io, err, "rmdir failed");
714+
let _ = Write::write_all(tx, &response).await;
715+
}
716+
}
701717
}
702718
x if x == Command::Rename as u8 => {
703719
let mut cursor = 0usize;

0 commit comments

Comments
 (0)