Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ jobs:
components: rustfmt
- run: cargo fmt --check

licenses:
name: License check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check licenses

clippy:
name: Clippy
runs-on: ubuntu-latest
Expand Down
22 changes: 22 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[licenses]
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"MIT",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"BSL-1.0",
"0BSD",
"CC0-1.0",
]
confidence-threshold = 0.8
unused-allowed-license = "allow"

[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
6 changes: 6 additions & 0 deletions src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ pub enum Action {

// Sorting
CycleSort,
SortByName(usize), // panel index
SortBySize(usize),
SortByDate(usize),

// Quick search
QuickSearch(char),
Expand Down Expand Up @@ -123,6 +126,9 @@ pub enum Action {
// Help
ShowHelp,

// Menu
OpenMenu,

// Settings
ToggleSettings,

Expand Down
179 changes: 177 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ pub struct App {
pub fuzzy_search: [Option<FuzzySearchState>; 2],
/// Help dialog state (scroll + optional search filter).
pub help_state: Option<HelpState>,
/// Menu bar state (F9 Far Manager style menu).
pub menu_state: Option<MenuState>,
/// Click ranges for menu titles in header bar: (x_start, x_end) per menu.
pub menu_title_ranges: Vec<(u16, u16)>,
/// Y coordinate of the header/menu bar (for click detection).
pub menu_bar_y: u16,
/// Settings dialog: selected item index, or None when closed.
pub settings_open: Option<usize>,
/// Shell panels (one per file panel side, like CI panels).
Expand Down Expand Up @@ -169,6 +175,14 @@ pub struct HelpState {
pub filter: String,
}

/// Menu bar state (F9 menu, Far Manager style).
pub struct MenuState {
/// Which top-level menu is open (0..MENU_COUNT-1).
pub active_menu: usize,
/// Selected item index within the dropdown.
pub selected_item: usize,
}

/// Overwrite confirmation dialog shown during Ask-mode copy/move.
pub struct OverwriteAskState {
pub focused: OverwriteAskChoice,
Expand Down Expand Up @@ -1555,6 +1569,9 @@ impl App {
goto_path: [None, None],
fuzzy_search: [None, None],
help_state: None,
menu_state: None,
menu_title_ranges: Vec::new(),
menu_bar_y: 0,
settings_open: None,
shell_panels: [None, None],
claude_panels: [None, None],
Expand Down Expand Up @@ -1964,6 +1981,21 @@ impl App {
};
}

// Menu bar intercepts keys
if self.menu_state.is_some() {
return match key.code {
KeyCode::Esc | KeyCode::F(9) => Action::DialogCancel,
KeyCode::Left => Action::CursorLeft,
KeyCode::Right => Action::CursorRight,
KeyCode::Up => Action::MoveUp,
KeyCode::Down => Action::MoveDown,
KeyCode::Home => Action::MoveToTop,
KeyCode::End => Action::MoveToBottom,
KeyCode::Enter => Action::DialogConfirm,
_ => Action::None,
};
}

// Goto-line prompt intercepts keys
if self.goto_line_input.is_some() {
return match key.code {
Expand Down Expand Up @@ -2587,7 +2619,13 @@ impl App {
}
}
KeyCode::F(8) => Action::Delete,
KeyCode::F(9) => Action::CycleSort,
KeyCode::F(9) => {
if key.modifiers.contains(KeyModifiers::CONTROL) {
Action::CycleSort
} else {
Action::OpenMenu
}
}
KeyCode::F(10) => Action::Quit,
KeyCode::F(2) => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
Expand Down Expand Up @@ -3528,6 +3566,51 @@ impl App {
return;
}

// Menu bar intercepts when active
if let Some(ref mut m) = self.menu_state {
let menu_count = crate::ui::menu::MENUS.len();
match action {
Action::DialogCancel => self.menu_state = None,
Action::CursorLeft => {
m.active_menu = if m.active_menu == 0 {
menu_count - 1
} else {
m.active_menu - 1
};
m.selected_item = 0;
}
Action::CursorRight => {
m.active_menu = (m.active_menu + 1) % menu_count;
m.selected_item = 0;
}
Action::MoveUp => {
let count = crate::ui::menu::selectable_count(m.active_menu);
m.selected_item = if m.selected_item == 0 {
count.saturating_sub(1)
} else {
m.selected_item - 1
};
}
Action::MoveDown => {
let count = crate::ui::menu::selectable_count(m.active_menu);
m.selected_item = (m.selected_item + 1) % count;
}
Action::MoveToTop => m.selected_item = 0,
Action::MoveToBottom => {
m.selected_item =
crate::ui::menu::selectable_count(m.active_menu).saturating_sub(1);
}
Action::DialogConfirm => {
if let Some(action) = crate::ui::menu::selected_action(m) {
self.menu_state = None;
self.handle_action(action);
}
}
_ => {}
}
return;
}

// Settings dialog intercepts when active
if self.settings_open.is_some() {
match action {
Expand Down Expand Up @@ -4167,6 +4250,14 @@ impl App {
});
}

// Menu
Action::OpenMenu => {
self.menu_state = Some(MenuState {
active_menu: 0,
selected_item: 0,
});
}

Action::ToggleSettings => {} // handled early, before bottom-panel dispatch

// CI failure extraction (only works when CI panel is focused, handled above)
Expand Down Expand Up @@ -4231,6 +4322,15 @@ impl App {
Action::CycleSort => {
self.active_panel_mut().cycle_sort();
}
Action::SortByName(side) => {
self.panels[side].set_sort(SortField::Name);
}
Action::SortBySize(side) => {
self.panels[side].set_sort(SortField::Size);
}
Action::SortByDate(side) => {
self.panels[side].set_sort(SortField::Date);
}

// CI / GitHub
Action::ToggleCi => {
Expand Down Expand Up @@ -8807,9 +8907,35 @@ impl App {
30 => FileSearchField::MaxCount,
31 => FileSearchField::MaxFileSize,
32 => FileSearchField::Encoding,
_ => FileSearchField::ButtonSearch,
_ => {
// Button row — pick Search or Cancel by x position,
// then fire the action immediately.
let cw = self
.dialog_content_area
.map(|a| a.width as usize)
.unwrap_or(60);
// Cancel button is on the right half
let btn = if x_off > cw / 2 {
FileSearchField::ButtonCancel
} else {
FileSearchField::ButtonSearch
};
state.focused = btn;
state.select_focused();
self.handle_file_search_dialog(Action::DialogConfirm);
return;
}
};
state.select_focused();
// Toggle checkbox on click (inputs just get focused/selected above)
if !state.focused.is_input()
&& !matches!(
state.focused,
FileSearchField::ButtonSearch | FileSearchField::ButtonCancel
)
{
state.toggle_focused();
}
return;
}
// Search dialog: query at y=2
Expand Down Expand Up @@ -8867,6 +8993,55 @@ impl App {
}

fn handle_mouse_click(&mut self, col: u16, row: u16) {
// Menu bar click detection (header row)
if row == self.menu_bar_y && matches!(self.mode, AppMode::Normal) {
for (i, &(x_start, x_end)) in self.menu_title_ranges.iter().enumerate() {
if col >= x_start && col < x_end {
let already_open = self.menu_state.as_ref().is_some_and(|m| m.active_menu == i);
self.menu_state = if already_open {
None
} else {
Some(MenuState {
active_menu: i,
selected_item: 0,
})
};
return;
}
}
// Clicked header but not on a title — close menu if open
if self.menu_state.is_some() {
self.menu_state = None;
return;
}
}

// Click on dropdown item when menu is open
if let Some(ref state) = self.menu_state {
let (sw, sh) = crossterm::terminal::size().unwrap_or((80, 24));
let screen = Rect::new(0, 0, sw, sh);
if let Some(sel) = crate::ui::menu::dropdown_click(
state,
&self.menu_title_ranges,
self.menu_bar_y,
screen,
col,
row,
) {
let clicked = MenuState {
active_menu: state.active_menu,
selected_item: sel,
};
if let Some(action) = crate::ui::menu::selected_action(&clicked) {
self.menu_state = None;
self.handle_action(action);
}
return;
}
// Click outside menu closes it
self.menu_state = None;
}

if let AppMode::Editing(ref mut e) = self.mode {
e.click_at(col, row);
return;
Expand Down
5 changes: 5 additions & 0 deletions src/panel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ impl Panel {
self.apply_sort();
}

pub fn set_sort(&mut self, field: SortField) {
self.sort_field = field;
self.apply_sort();
}

pub fn jump_to_match(&mut self, query: &str) {
let query_lower = query.to_lowercase();
if let Some(idx) = self
Expand Down
2 changes: 1 addition & 1 deletion src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ impl Theme {
let text = Color::Rgb(232, 228, 237); // #e8e4ed cool light grey
let text_dim = Color::Rgb(154, 143, 176); // #9a8fb0 muted lavender
let text_very_dim = Color::Rgb(107, 90, 132); // #6b5a84 dim purple
let selection = Color::Rgb(46, 39, 64); // #2e2740 selection bg
let selection = Color::Rgb(74, 50, 100); // #4a3264 selection bg
let green = Color::Rgb(130, 210, 150); // #82d296 soft green
let blue = Color::Rgb(130, 160, 230); // #82a0e6 soft blue
let red = Color::Rgb(240, 100, 100); // #f06464 soft red
Expand Down
2 changes: 1 addition & 1 deletion src/ui/footer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub fn render(frame: &mut Frame, area: Rect) {
("F6", "RnMov"),
("F7", "MkFld"),
("F8", "Del"),
("F9", "Sort"),
("F9", "Menu"),
("F10", "Quit"),
("F12", "Claude"),
];
Expand Down
3 changes: 2 additions & 1 deletion src/ui/help_dialog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const HELP_SECTIONS: &[(&str, &[(&str, &str)])] = &[
("Shift+F7", "Create (touch) file"),
("Shift+F5", "Create archive (tar.zst/gz/xz/zip)"),
("F8", "Delete file/selection"),
("F9", "Cycle sort (name/size/date)"),
("Ctrl+F9", "Cycle sort (name/size/date)"),
],
),
(
Expand Down Expand Up @@ -244,6 +244,7 @@ const HELP_SECTIONS: &[(&str, &[(&str, &str)])] = &[
&[
("F1", "This help screen"),
("Shift+F1", "Settings (theme, etc.)"),
("F9", "Menu"),
("F10 / Ctrl+Q", "Quit (with confirmation)"),
("F11", "Open PR in browser"),
],
Expand Down
Loading
Loading