Skip to content

Commit e18bdbf

Browse files
committed
Brief mode: User setting for max column width
- New `listing.briefColumnWidthMode` setting (radio): `paneWidth` (default) lets columns fill the pane; `limited` caps them at a user-chosen pixel value. - New `listing.briefColumnWidthMaxPx` setting (slider, 250-1000 px, default 400): the cap value, disabled when mode is `paneWidth`. - Replaces the hardcoded `MAX_BRIEF_COLUMN_WIDTH = 300` constant in `BriefList.svelte` with a reactive `capPx = min(containerWidth, userCap)` derivation. Container width is always the outer ceiling — a column wider than the pane is never useful. - Description text explains the min-with-pane-width behavior so the slider's role is clear. - Wired through `reactive-settings.svelte.ts` (new `BriefColumnWidthMode` type, getters, change subscriptions) and rendered in `ListingSection.svelte` as one row: radio + slider stacked, slider visually nested and disabled when mode is `paneWidth`. - `BriefList.a11y.test.ts` mock updated for the two new exports.
1 parent 44c72f5 commit e18bdbf

8 files changed

Lines changed: 124 additions & 14 deletions

File tree

apps/desktop/src/lib/file-explorer/views/BriefList.a11y.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ vi.mock('$lib/settings/reactive-settings.svelte', () => ({
4444
getStripedRows: () => false,
4545
getHumanFriendlySizeUnits: () => false,
4646
getFileSizeFormat: () => 'binary',
47+
getBriefColumnWidthMode: () => 'paneWidth',
48+
getBriefColumnWidthMaxPx: () => 400,
4749
}))
4850

4951
vi.mock('$lib/settings/settings-store', () => ({

apps/desktop/src/lib/file-explorer/views/BriefList.svelte

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@
2323
import { ensureFontMetricsLoaded, getCurrentFontId } from '$lib/font-metrics'
2424
import { getDirStatsBatch } from '$lib/tauri-commands'
2525
import { buildDirSizeTooltip, hasSizeMismatch } from './full-list-utils'
26-
import { getRowHeight, formatFileSize, getSizeMismatchWarning, getStripedRows } from '$lib/settings/reactive-settings.svelte'
26+
import {
27+
getRowHeight,
28+
formatFileSize,
29+
getSizeMismatchWarning,
30+
getStripedRows,
31+
getBriefColumnWidthMode,
32+
getBriefColumnWidthMaxPx,
33+
} from '$lib/settings/reactive-settings.svelte'
2734
import { onDebouncedScaleChange } from '$lib/text-size.svelte'
2835
import { getSetting } from '$lib/settings/settings-store'
2936
import { formatNumber, pluralize } from '../selection/selection-info-utils'
@@ -119,8 +126,6 @@
119126
// Buffer columns is reactive based on settings
120127
const bufferColumns = $derived(getSetting('advanced.virtualizationBufferColumns'))
121128
const MIN_COLUMN_WIDTH = 100
122-
/** Hard cap for any single Brief column. Container width may further clamp this. */
123-
const MAX_BRIEF_COLUMN_WIDTH = 300
124129
// Add space for: icon (16px) + gap (8px) + left padding (8px) + right padding (8px) + rounding buffer (2px)
125130
// The 2px buffer accounts for sub-pixel rendering differences between calculated and actual widths.
126131
const COLUMN_PADDING = 16 + 8 + 8 + 8 + 2
@@ -147,13 +152,18 @@
147152
const totalColumns = $derived(Math.ceil(totalCount / itemsPerColumn))
148153
149154
/**
150-
* Cap applied to each column AFTER chrome is added. Tracks live container size,
151-
* with a hard ceiling at `MAX_BRIEF_COLUMN_WIDTH` so absurdly wide panes still
152-
* cap a column at a readable width.
155+
* Cap applied to each column AFTER chrome is added.
156+
*
157+
* - 'paneWidth' mode (default): columns can grow to fill the pane.
158+
* - 'limited' mode: columns also can't exceed the user-chosen pixel cap.
159+
*
160+
* `containerWidth` is always the outer ceiling — a column wider than the pane has no value.
153161
*/
154-
const capPx = $derived(
155-
containerWidth > 0 ? Math.min(containerWidth, MAX_BRIEF_COLUMN_WIDTH) : MAX_BRIEF_COLUMN_WIDTH,
156-
)
162+
const capPx = $derived.by(() => {
163+
const userCap = getBriefColumnWidthMode() === 'limited' ? getBriefColumnWidthMaxPx() : Number.POSITIVE_INFINITY
164+
if (containerWidth <= 0) return Math.min(userCap, 1000)
165+
return Math.min(containerWidth, userCap)
166+
})
157167
158168
/**
159169
* Running cumulative width totals: `prefixSums[i] = sum(widths[0..i))`.

apps/desktop/src/lib/settings/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type {
1313
EnumOption,
1414
DirectorySortMode,
1515
SizeDisplayMode,
16+
BriefColumnWidthMode,
1617
ExtensionChangePolicy,
1718
FileSizeFormat,
1819
NetworkTimeoutMode,

apps/desktop/src/lib/settings/reactive-settings.svelte.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type FileSizeFormat,
1313
type DirectorySortMode,
1414
type SizeDisplayMode,
15+
type BriefColumnWidthMode,
1516
type AppColor,
1617
densityMappings,
1718
} from '$lib/settings'
@@ -34,6 +35,8 @@ let sizeDisplay = $state<SizeDisplayMode>('smart')
3435
let humanFriendlySizeUnits = $state<boolean>(true)
3536
let sizeMismatchWarning = $state<boolean>(true)
3637
let stripedRows = $state<boolean>(false)
38+
let briefColumnWidthMode = $state<BriefColumnWidthMode>('paneWidth')
39+
let briefColumnWidthMaxPx = $state<number>(400)
3740
let networkEnabled = $state<boolean>(true)
3841
let typeToJumpResetDelay = $state<number>(1000)
3942

@@ -63,6 +66,8 @@ export async function initReactiveSettings(): Promise<void> {
6366
humanFriendlySizeUnits = getSetting('listing.humanFriendlySizeUnits')
6467
sizeMismatchWarning = getSetting('listing.sizeMismatchWarning')
6568
stripedRows = getSetting('listing.stripedRows')
69+
briefColumnWidthMode = getSetting('listing.briefColumnWidthMode')
70+
briefColumnWidthMaxPx = getSetting('listing.briefColumnWidthMaxPx')
6671
networkEnabled = getSetting('network.enabled')
6772
typeToJumpResetDelay = getSetting('fileExplorer.typeToJump.resetDelay')
6873

@@ -106,6 +111,12 @@ export async function initReactiveSettings(): Promise<void> {
106111
case 'listing.stripedRows':
107112
stripedRows = value as boolean
108113
break
114+
case 'listing.briefColumnWidthMode':
115+
briefColumnWidthMode = value as BriefColumnWidthMode
116+
break
117+
case 'listing.briefColumnWidthMaxPx':
118+
briefColumnWidthMaxPx = value as number
119+
break
109120
case 'network.enabled':
110121
networkEnabled = value as boolean
111122
break
@@ -206,6 +217,16 @@ export function getStripedRows(): boolean {
206217
return stripedRows
207218
}
208219

220+
/** Get the Brief mode column-width mode: 'paneWidth' lets columns fill the pane; 'limited' caps them at `getBriefColumnWidthMaxPx`. */
221+
export function getBriefColumnWidthMode(): BriefColumnWidthMode {
222+
return briefColumnWidthMode
223+
}
224+
225+
/** Get the Brief mode column-width pixel limit (applies only when mode is 'limited'). */
226+
export function getBriefColumnWidthMaxPx(): number {
227+
return briefColumnWidthMaxPx
228+
}
229+
209230
/** Get whether networking (SMB discovery + connections) is enabled. */
210231
export function getNetworkEnabled(): boolean {
211232
return networkEnabled

apps/desktop/src/lib/settings/sections/ListingSection.svelte

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
import SettingRow from '../components/SettingRow.svelte'
44
import SettingToggleGroup from '../components/SettingToggleGroup.svelte'
55
import SettingSwitch from '../components/SettingSwitch.svelte'
6+
import SettingRadioGroup from '../components/SettingRadioGroup.svelte'
7+
import SettingSlider from '../components/SettingSlider.svelte'
68
import { getSettingDefinition } from '$lib/settings'
79
import { createShouldShow } from '$lib/settings/settings-search'
10+
import { getBriefColumnWidthMode } from '$lib/settings/reactive-settings.svelte'
811
912
interface Props {
1013
searchQuery: string
@@ -22,6 +25,9 @@
2225
}
2326
const sizeMismatchDef = getSettingDefinition('listing.sizeMismatchWarning') ?? { label: '', description: '' }
2427
const stripedRowsDef = getSettingDefinition('listing.stripedRows') ?? { label: '', description: '' }
28+
const briefWidthModeDef = getSettingDefinition('listing.briefColumnWidthMode') ?? { label: '', description: '' }
29+
30+
const sliderDisabled = $derived(getBriefColumnWidthMode() !== 'limited')
2531
</script>
2632

2733
<SettingsSection title="Listing">
@@ -75,4 +81,37 @@
7581
<SettingSwitch id="listing.stripedRows" />
7682
</SettingRow>
7783
{/if}
84+
{#if shouldShow('listing.briefColumnWidthMode')}
85+
<SettingRow
86+
id="listing.briefColumnWidthMode"
87+
label={briefWidthModeDef.label}
88+
description={briefWidthModeDef.description}
89+
{searchQuery}
90+
>
91+
<div class="brief-width-control">
92+
<SettingRadioGroup id="listing.briefColumnWidthMode" />
93+
<div class="slider-row" class:is-disabled={sliderDisabled}>
94+
<SettingSlider id="listing.briefColumnWidthMaxPx" unit="px" disabled={sliderDisabled} />
95+
</div>
96+
</div>
97+
</SettingRow>
98+
{/if}
7899
</SettingsSection>
100+
101+
<style>
102+
.brief-width-control {
103+
display: flex;
104+
flex-direction: column;
105+
gap: var(--spacing-sm);
106+
width: 100%;
107+
}
108+
109+
.slider-row {
110+
/* Visually nests the slider under the radio choices. */
111+
padding-left: var(--spacing-xl);
112+
}
113+
114+
.slider-row.is-disabled {
115+
opacity: 0.5;
116+
}
117+
</style>

apps/desktop/src/lib/settings/settings-registry.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,40 @@ export const settingsRegistry: SettingDefinition[] = [
233233
component: 'switch',
234234
},
235235

236+
{
237+
id: 'listing.briefColumnWidthMode',
238+
section: ['General', 'Listing'],
239+
label: 'Maximum column width in Brief mode',
240+
description:
241+
'Limits how wide Brief mode columns can grow to fit long filenames. Columns are always capped at the pane width regardless — the chosen limit only kicks in when it would be smaller than the pane.',
242+
keywords: ['brief', 'column', 'width', 'max', 'maximum', 'limit', 'pane', 'shrink-wrap'],
243+
type: 'enum',
244+
default: 'paneWidth',
245+
component: 'radio',
246+
constraints: {
247+
options: [
248+
{ value: 'paneWidth', label: 'Pane width (no limit)' },
249+
{ value: 'limited', label: 'Limit to' },
250+
],
251+
},
252+
},
253+
{
254+
id: 'listing.briefColumnWidthMaxPx',
255+
section: ['General', 'Listing'],
256+
label: 'Brief column width limit',
257+
description: '',
258+
keywords: ['brief', 'column', 'width', 'max', 'maximum', 'limit', 'pixel', 'slider'],
259+
type: 'number',
260+
default: 400,
261+
component: 'slider',
262+
constraints: {
263+
min: 250,
264+
max: 1000,
265+
step: 25,
266+
sliderStops: [250, 400, 600, 800, 1000],
267+
},
268+
},
269+
236270
// ========================================================================
237271
// General › Git
238272
// ========================================================================

apps/desktop/src/lib/settings/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ export type ThemeMode = 'light' | 'dark' | 'system'
8484
export type ExtensionChangePolicy = 'yes' | 'no' | 'ask'
8585
export type DirectorySortMode = 'likeFiles' | 'alwaysByName'
8686
export type SizeDisplayMode = 'smart' | 'logical' | 'physical'
87+
export type BriefColumnWidthMode = 'paneWidth' | 'limited'
8788
export type AppColor = 'system' | 'cmdr-gold'
8889
export type SizeColorsPalette = 'none' | 'app' | 'rainbow'
8990
export type DateColorsPalette = 'off' | 'app' | 'wilting'
@@ -108,6 +109,8 @@ export interface SettingsValues {
108109
'listing.humanFriendlySizeUnits': boolean
109110
'listing.sizeMismatchWarning': boolean
110111
'listing.stripedRows': boolean
112+
'listing.briefColumnWidthMode': BriefColumnWidthMode
113+
'listing.briefColumnWidthMaxPx': number
111114

112115
// Git
113116
'fileExplorer.git.showRepoChip': boolean

apps/desktop/test/e2e-playwright/CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ pointing to a fixture directory created by `e2e-shared/fixtures.ts`.
6262

6363
## Running a single spec
6464

65-
When iterating on one spec, **run only that spec**. The full suite takes ~10 minutes and produces noisy cascade
66-
failures when the broken test takes the app down with it (subsequent specs fail with connection errors). Save the
67-
full run for the final CI-green check.
65+
When iterating on one spec, **run only that spec**. The full suite takes ~10 minutes and produces noisy cascade failures
66+
when the broken test takes the app down with it (subsequent specs fail with connection errors). Save the full run for
67+
the final CI-green check.
6868

6969
With the app already running (see "Manually" above), filter by file or by name:
7070

@@ -78,8 +78,8 @@ CMDR_E2E_START_PATH=/tmp/cmdr-e2e-fixtures pnpm test:e2e:playwright test/e2e-pla
7878
CMDR_E2E_START_PATH=/tmp/cmdr-e2e-fixtures pnpm test:e2e:playwright --grep "cursor stays in view"
7979
```
8080

81-
The checker invocation (`./scripts/check.sh --check desktop-e2e-playwright`) doesn't support filtering — it always
82-
runs the whole suite. So during iteration, prefer the manual flow.
81+
The checker invocation (`./scripts/check.sh --check desktop-e2e-playwright`) doesn't support filtering — it always runs
82+
the whole suite. So during iteration, prefer the manual flow.
8383

8484
## Running on Linux (Docker)
8585

0 commit comments

Comments
 (0)