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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ That's it. Type to search, arrow keys to navigate, Enter to launch.

Need install variants, launch methods, or mode-specific examples? See [USAGE.md](./USAGE.md).

Make the layout yours: dock panels around the results, add independent dmenu previews, move them
interactively, or switch the launcher to an icon grid with distinct pinned-row colors.
All layouts are opt-in; see [panel and grid options](./USAGE.md#panel-layouts).


## Install

Expand Down
40 changes: 37 additions & 3 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ Note: Bare `FSEL_*` launcher keys set root defaults. `[app_launcher]` in `config

**`[app_launcher]` overrides (`FSEL_APP_LAUNCHER_*`):**

`FILTER_DESKTOP`, `FILTER_ACTIONS`, `LIST_EXECUTABLES_IN_PATH`, `HIDE_BEFORE_TYPING`, `LAUNCH_PREFIX`, `MATCH_MODE`, `RANKING_MODE`, `PINNED_ORDER`, `CONFIRM_FIRST_LAUNCH`, `PREFIX_DEPTH`, `ICON_MODE`, `ICON_POSITION`, `ICON_DESCRIPTION_POSITION`, `ICON_PREVIEW_WIDTH_PERCENT`, `ICON_LIST_WIDTH`, `ICON_LIST_HEIGHT`, `ICON_LIST_GAP`, `ICON_LIST_VERTICAL_ALIGN_PERCENT`, `ICON_ARROW_BEFORE`, `ICON_SIZE`, `ICON_HORIZONTAL_ALIGN_PERCENT`, `ICON_VERTICAL_ALIGN_PERCENT`, `ICON_THEME` (each prefixed with `FSEL_APP_LAUNCHER_`)
`FILTER_DESKTOP`, `FILTER_ACTIONS`, `LIST_EXECUTABLES_IN_PATH`, `HIDE_BEFORE_TYPING`, `LAUNCH_PREFIX`, `MATCH_MODE`, `RANKING_MODE`, `PINNED_ORDER`, `CONFIRM_FIRST_LAUNCH`, `PREFIX_DEPTH`, `GRID_COLUMNS`, `GRID_ROW_HEIGHT`, `ICON_MODE`, `ICON_POSITION`, `ICON_DESCRIPTION_POSITION`, `ICON_PREVIEW_WIDTH_PERCENT`, `ICON_LIST_WIDTH`, `ICON_LIST_HEIGHT`, `ICON_LIST_GAP`, `ICON_LIST_VERTICAL_ALIGN_PERCENT`, `ICON_ARROW_BEFORE`, `ICON_SIZE`, `ICON_HORIZONTAL_ALIGN_PERCENT`, `ICON_VERTICAL_ALIGN_PERCENT`, `ICON_THEME` (each prefixed with `FSEL_APP_LAUNCHER_`)

Keybinds are not configurable via environment variables; use `~/.config/fsel/keybinds.toml` or the `[keybinds]` section in `config.toml`. When both are present, the embedded `[keybinds]` section takes precedence.

Expand Down Expand Up @@ -721,7 +721,7 @@ This means you've placed a **color/UI option inside the [app_launcher] section**
- General: `terminal_launcher` (use `"tty"` for TTY mode, same as -t/--tty), `keybinds`

**[app_launcher] Section (strict validation):**
- `filter_desktop`, `filter_actions`, `auto_hide_duplicates`, `list_executables_in_path`, `hide_before_typing`, `match_mode`, `ranking_mode`, `pinned_order`, `confirm_first_launch`, `prefix_depth`, `icon_mode`, `icon_position`, `icon_description_position`, `icon_preview_width_percent`, `icon_list_width`, `icon_list_height`, `icon_list_gap`, `icon_list_vertical_align_percent`, `icon_arrow_before`, `icon_size`, `icon_horizontal_align_percent`, `icon_vertical_align_percent`, `icon_theme`
- `filter_desktop`, `filter_actions`, `auto_hide_duplicates`, `list_executables_in_path`, `hide_before_typing`, `match_mode`, `ranking_mode`, `pinned_order`, `confirm_first_launch`, `prefix_depth`, `grid_columns`, `grid_row_height`, `icon_mode`, `icon_position`, `icon_description_position`, `icon_preview_width_percent`, `icon_list_width`, `icon_list_height`, `icon_list_gap`, `icon_list_vertical_align_percent`, `icon_arrow_before`, `icon_size`, `icon_horizontal_align_percent`, `icon_vertical_align_percent`, `icon_theme`

**[dmenu] Section:**
- Colors: `highlight_color`, `main_border_color`, `items_border_color`, `input_border_color`, `main_text_color`, `items_text_color`, `input_text_color`, `header_title_color`
Expand All @@ -738,7 +738,41 @@ This means you've placed a **color/UI option inside the [app_launcher] section**
- Images: `image_preview`, `hide_inline_image_message`
# Panel layouts

## Pinned application colors
## Application grid

The launcher has an opt-in grid; ordinary lists, dmenu, and cclip retain their layouts.

```sh
fsel --no-exec --app-grid 4 --desktop-icons=both --grid-row-height 4
fsel --no-exec --app-grid 4 --desktop-icons=list --info-position left --info-size 30
```

`--app-grid` requests up to 64 columns; zero disables the grid. Narrow panels reduce the column
count to keep cells at least eight columns wide where possible. `--grid-row-height` sets cell
height from 2–16 terminal rows (default 4). With list icons enabled, artwork occupies the upper
rows and the label/selection marker occupies the last row. Without list icons, the grid shows
text only. Names are clipped to their own cells; icons retain proportional sizing and the existing
normalization/cache behavior. `--icon-list-width` controls artwork width; the ordinary list's
height, gap, and arrow-before placement do not change grid geometry. Negative artwork alignment
retains its documented opt-in overflow behavior.

At rotation zero, Left/Right move one item and Up/Down move one grid row. Quarter turns use
column-first ordering, and arrow movement follows the visible orientation. Tab/Shift+Tab move
one logical item. Search, mouse selection, pins, pinned colors, and backgrounds work normally.
Scrolling advances a complete grid row (or column after a quarter turn); resizing keeps the
selected item visible. With `hard_stop`, moves beyond available items stop; otherwise they wrap.

```toml
[app_launcher]
grid_columns = 4
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
grid_row_height = 4
Comment thread
Mjoyufull marked this conversation as resolved.
icon_mode = "both"
```

Environment overrides are `FSEL_APP_LAUNCHER_GRID_COLUMNS` and
`FSEL_APP_LAUNCHER_GRID_ROW_HEIGHT`. These launcher-specific settings do not change dmenu or cclip.

## Pinned row styling

Pinned launcher rows can use their own text and background colors, independently of the pin glyph.
Put these optional settings at the root of the configuration, outside `[app_launcher]`:
Expand Down
5 changes: 5 additions & 0 deletions config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ title_panel_position = "top"
# WARNING: DO NOT put color/UI options here - they belong at the root level!
[app_launcher]

# Grid columns: 0 keeps the normal list; 1-64 requests a grid that fits the available width
grid_columns = 0
# Rows per grid cell, including the label (2-16); ignored while the grid is disabled
grid_row_height = 4

# Filter apps based on OnlyShowIn/NotShowIn and $XDG_CURRENT_DESKTOP
# Hides apps not meant for your desktop environment
filter_desktop = true
Expand Down
7 changes: 7 additions & 0 deletions fsel.1
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,9 @@ Set to \fBtrue\fR to enable deterministic duplicate suppression in app launcher
.TP
.B FSEL_APP_LAUNCHER_ICON_*
Override icon mode, position, preview width, lookup size, or theme. See config.toml for the corresponding keys.
.TP
.B FSEL_APP_LAUNCHER_GRID_COLUMNS, FSEL_APP_LAUNCHER_GRID_ROW_HEIGHT
Set grid columns (0 disables, maximum 64) and cell height (2-16 rows).
.SH FILES
.TP
.I ~/.config/fsel/config.toml
Expand Down Expand Up @@ -614,3 +617,7 @@ placeholders and cancellation behavior as --preview. Zero percent hides a panel.
--panel-edit enables Alt+P layout editing. Tab cycles panels, arrows dock,
plus/minus resize, and Escape returns to selection. Mouse dragging docks panels;
the wheel resizes the focused panel. Changes are session-only.
.PP
--app-grid COLUMNS enables a launcher grid (0 disables, maximum 64).
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
--grid-row-height ROWS sets cell height (2-16, default 4). List icons appear
above labels. Arrow navigation and mouse selection follow grid cells.
6 changes: 6 additions & 0 deletions src/cli/from_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ fn apply_app_launcher_overrides(default: &mut Opts, fsel_config: &FselConfig) {
if let Some(height) = fsel_config.app_launcher.icon_list_height {
default.desktop_icon_list_height = height;
}
if let Some(columns) = fsel_config.app_launcher.grid_columns {
default.app_grid_columns = columns;
}
if let Some(height) = fsel_config.app_launcher.grid_row_height {
default.app_grid_row_height = height;
}
if let Some(gap) = fsel_config.app_launcher.icon_list_gap {
default.desktop_icon_list_gap = gap;
}
Expand Down
8 changes: 6 additions & 2 deletions src/cli/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ Usage:
│ ├─ --info-size <PERCENT> Information panel share, 0-90
│ ├─ --input-size <CELLS> Input thickness in rows or columns
│ ├─ --layout-rotation <DEGREES> Rotate layout: 0, 90, 180, 270
│ └─ --item-width <COLUMNS> Horizontal result width (default 24)
│ ├─ --item-width <COLUMNS> Horizontal result width (default 24)
│ ├─ --app-grid <COLUMNS> Opt-in app grid, 0 disables (maximum 64)
│ └─ --grid-row-height <ROWS> Grid cell height, 2-16 (default 4)
│
├─ Core Modes
│ ├─ -p, --program <NAME> Launch one app immediately; exact mode refuses near matches
Expand Down Expand Up @@ -83,7 +85,9 @@ Usage:
│ ├─ --info-size <PERCENT> Information panel share, 0-90
│ ├─ --input-size <CELLS> Input thickness in rows or columns
│ ├─ --layout-rotation <DEGREES> Rotate layout: 0, 90, 180, 270
│ └─ --item-width <COLUMNS> Horizontal result width (default 24)
│ ├─ --item-width <COLUMNS> Horizontal result width (default 24)
│ ├─ --app-grid <COLUMNS> Opt-in app grid, 0 disables (maximum 64)
│ └─ --grid-row-height <ROWS> Grid cell height, 2-16 (default 4)
│
├─ Core Modes
│ ├─ -p, --program <NAME> Launch one app immediately; exact mode requires an exact hit
Expand Down
10 changes: 10 additions & 0 deletions src/cli/parse/overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ pub(super) fn parse_cli_overrides(

while let Some(arg) = parser.next()? {
match arg {
Long("app-grid") => {
default.app_grid_columns = value_as_string(parser, "Invalid grid columns")?
.parse()
.map_err(|_| CliError::message("grid columns must be an integer"))?;
}
Long("grid-row-height") => {
default.app_grid_row_height = value_as_string(parser, "Invalid grid height")?
.parse()
.map_err(|_| CliError::message("grid row height must be an integer"))?;
}
Long("panel-edit") => {
default.dmenu_panel_edit = true;
default.dmenu_mode = true;
Expand Down
4 changes: 4 additions & 0 deletions src/cli/types/opts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use crate::ui::{HorizontalPosition, InputPanelStyle, PanelPosition};
pub struct Opts {
pub dmenu_panels: Vec<crate::modes::dmenu::panels::DmenuPanel>,
pub dmenu_panel_edit: bool,
pub app_grid_columns: u16,
pub app_grid_row_height: u16,
pub panels: crate::ui::PanelSettings,
pub pinned_text_color: Option<ratatui::style::Color>,
pub pinned_background_color: Option<ratatui::style::Color>,
Expand Down Expand Up @@ -157,6 +159,8 @@ impl Default for Opts {
Self {
dmenu_panels: Vec::new(),
dmenu_panel_edit: false,
app_grid_columns: 0,
app_grid_row_height: 4,
panels: crate::ui::PanelSettings::default(),
pinned_text_color: None,
pinned_background_color: None,
Expand Down
35 changes: 35 additions & 0 deletions src/cli/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ pub(super) fn validate(default: &mut Opts, cli_launch_methods: usize) -> Result<
&& !default.clear_history
&& !default.clear_cache
&& !default.refresh_cache;
if uses_desktop_icons
&& default.app_grid_columns > 0
&& (default.app_grid_columns > 64 || !(2..=16).contains(&default.app_grid_row_height))
{
return Err(CliError::message(
"grid columns must be 1-64 and grid row height must be 2-16",
));
}
if uses_desktop_icons
&& default.desktop_icon_mode.shows_preview()
&& !(10..=90).contains(&default.desktop_icon_preview_width_percent)
Expand Down Expand Up @@ -79,6 +87,7 @@ pub(super) fn validate(default: &mut Opts, cli_launch_methods: usize) -> Result<
}
if uses_desktop_icons
&& default.desktop_icon_mode.shows_list()
&& default.app_grid_columns == 0
&& !(1..=8).contains(&default.desktop_icon_list_height)
{
return Err(CliError::message(
Expand All @@ -87,6 +96,7 @@ pub(super) fn validate(default: &mut Opts, cli_launch_methods: usize) -> Result<
}
if uses_desktop_icons
&& default.desktop_icon_mode.shows_list()
&& default.app_grid_columns == 0
&& default.desktop_icon_list_gap > 16
{
return Err(CliError::message(
Expand Down Expand Up @@ -200,6 +210,31 @@ mod tests {
use super::validate;
use crate::cli::{DesktopIconMode, Opts};

#[test]
fn grid_dimensions_are_validated_only_when_active() {
for (columns, height) in [(65, 4), (4, 1), (4, 17)] {
let mut cli = Opts {
app_grid_columns: columns,
app_grid_row_height: height,
..Default::default()
};
assert!(validate(&mut cli, 0).is_err());
}
let mut disabled = Opts {
app_grid_row_height: 0,
..Default::default()
};
assert!(validate(&mut disabled, 0).is_ok());
let mut grid = Opts {
app_grid_columns: 4,
desktop_icon_mode: DesktopIconMode::List,
desktop_icon_list_height: 0,
desktop_icon_list_gap: 99,
..Default::default()
};
assert!(validate(&mut grid, 0).is_ok());
}

#[test]
fn reject_both_index_modes() {
let mut cli = Opts {
Expand Down
12 changes: 12 additions & 0 deletions src/config/env/app_launcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ pub(super) fn apply(cfg: &mut FselConfig, source: &impl OverrideSource) -> Resul
&mut cfg.app_launcher.icon_description_position,
"top, right, bottom, or left",
)?;
set_optional_parsed(
source,
"FSEL_APP_LAUNCHER_GRID_COLUMNS",
&mut cfg.app_launcher.grid_columns,
INTEGER_EXPECTED,
)?;
set_optional_parsed(
source,
"FSEL_APP_LAUNCHER_GRID_ROW_HEIGHT",
&mut cfg.app_launcher.grid_row_height,
INTEGER_EXPECTED,
)?;
set_optional_parsed(
source,
"FSEL_APP_LAUNCHER_FILTER_DESKTOP",
Expand Down
2 changes: 2 additions & 0 deletions src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub struct FselConfig {
/// Legacy `[app_launcher]` section for backward compatibility.
#[derive(Debug, Deserialize, Clone, Default)]
pub struct AppLauncherConfig {
pub grid_columns: Option<u16>,
pub grid_row_height: Option<u16>,
pub filter_desktop: Option<bool>,
pub filter_actions: Option<bool>,
pub auto_hide_duplicates: Option<bool>,
Expand Down
76 changes: 74 additions & 2 deletions src/modes/app_launcher/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,34 @@ fn handle_key_event(
state.should_exit = true;
}

if cli.app_grid_columns > 0 && matches!(msg, Message::MoveUp | Message::MoveDown) {
let layout = crate::ui::launcher_result_layout(terminal_area, cli);
if let Some(selected) = state.selected {
let vertical = cli.keybinds.matches_up(key.code, key.modifiers)
|| cli.keybinds.matches_down(key.code, key.modifiers);
let backwards = matches!(msg, Message::MoveUp) ^ (cli.panels.rotation >= 180);
let next = grid_neighbor(
selected,
state.shown.len(),
if matches!(key.code, KeyCode::Tab | KeyCode::BackTab) {
1
} else {
layout.navigation_step(vertical)
},
backwards,
cli.hard_stop,
);
crate::core::state::update(
state,
Message::SelectIndex(next),
cli.hard_stop,
max_visible,
);
}
layout.keep_visible(state.selected, &mut state.scroll_offset);
refresh_info(state, cli);
return;
}
let oriented = if cli.panels.rotation >= 180 {
match msg {
Message::MoveUp => Message::MoveDown,
Expand All @@ -109,6 +137,8 @@ fn handle_key_event(
msg
};
crate::core::state::update(state, oriented, cli.hard_stop, max_visible);
crate::ui::launcher_result_layout(terminal_area, cli)
.keep_visible(state.selected, &mut state.scroll_offset);
refresh_info(state, cli);
}

Expand Down Expand Up @@ -149,7 +179,7 @@ fn handle_mouse_event(
&& !state.shown.is_empty()
&& state.scroll_offset + metrics.capacity() < state.shown.len()
{
state.scroll_offset += 1;
state.scroll_offset = state.scroll_offset.saturating_add(metrics.scroll_step());
if let Some(index) = metrics.hit(mouse_event.column, mouse_event.row) {
state.selected = Some((state.scroll_offset + index).min(state.shown.len() - 1));
}
Expand All @@ -162,7 +192,7 @@ fn handle_mouse_event(
&& !state.shown.is_empty()
&& state.scroll_offset > 0
{
state.scroll_offset -= 1;
state.scroll_offset = state.scroll_offset.saturating_sub(metrics.scroll_step());
if let Some(index) = metrics.hit(mouse_event.column, mouse_event.row) {
state.selected = Some((state.scroll_offset + index).min(state.shown.len() - 1));
}
Expand All @@ -183,6 +213,36 @@ fn handle_mouse_event(
}
}

fn grid_neighbor(
selected: usize,
len: usize,
step: usize,
backwards: bool,
hard_stop: bool,
) -> usize {
if len == 0 {
return selected;
}
let next = if backwards {
selected.checked_sub(step)
} else {
selected.checked_add(step).filter(|index| *index < len)
};
if let Some(next) = next {
return next;
}
if hard_stop {
return selected;
}
let step = step.max(1);
let axis_start = selected % step;
if backwards {
axis_start + ((len - 1 - axis_start) / step) * step
} else {
axis_start
}
}

fn toggle_selected_pin(state: &mut State, db: &std::sync::Arc<redb::Database>) {
let Some(index) = state.selected else {
return;
Expand Down Expand Up @@ -277,3 +337,15 @@ fn list_metrics(
) -> crate::ui::result_layout::ResultLayout {
crate::ui::launcher_result_layout(terminal_area, cli)
}

#[cfg(test)]
mod grid_tests {
#[test]
fn grid_navigation_steps_by_row_and_respects_hard_stop() {
assert_eq!(super::grid_neighbor(2, 11, 4, false, false), 6);
assert_eq!(super::grid_neighbor(2, 11, 4, true, true), 2);
assert_eq!(super::grid_neighbor(2, 11, 4, true, false), 10);
assert_eq!(super::grid_neighbor(9, 11, 4, false, false), 1);
assert_eq!(super::grid_neighbor(9, 11, 4, false, true), 9);
}
}
Loading
Loading