@@ -4,8 +4,8 @@ import * as fs from 'fs';
44import * as os from 'os' ;
55import { spawn } from 'child_process' ;
66import * as pty from 'node-pty' ;
7- import { FileHandler } from './fileHandler' ;
8- import { IPC , SearchOptions , Bookmark , Highlight } from '../shared/types' ;
7+ import { FileHandler , filterLineToVisibleColumns , ColumnConfig } from './fileHandler' ;
8+ import { IPC , SearchOptions , Bookmark , Highlight , HighlightGroup } from '../shared/types' ;
99import { analyzerRegistry , AnalyzerOptions } from './analyzers' ;
1010
1111let mainWindow : BrowserWindow | null = null ;
@@ -47,6 +47,7 @@ const highlights = new Map<string, Highlight>();
4747// Config folder path (~/.logan/)
4848const getConfigDir = ( ) => path . join ( os . homedir ( ) , '.logan' ) ;
4949const getHighlightsPath = ( ) => path . join ( getConfigDir ( ) , 'highlights.json' ) ;
50+ const getHighlightGroupsPath = ( ) => path . join ( getConfigDir ( ) , 'highlight-groups.json' ) ;
5051const getBookmarksPath = ( ) => path . join ( getConfigDir ( ) , 'bookmarks.json' ) ;
5152
5253// Ensure config directory exists
@@ -235,9 +236,17 @@ function saveBookmarksForCurrentFile(): void {
235236}
236237
237238function createWindow ( ) {
239+ const isMac = process . platform === 'darwin' ;
240+
238241 mainWindow = new BrowserWindow ( {
239242 width : 1400 ,
240243 height : 900 ,
244+ show : false ,
245+ frame : false ,
246+ ...( isMac ? {
247+ titleBarStyle : 'hidden' ,
248+ trafficLightPosition : { x : 10 , y : 6 } ,
249+ } : { } ) ,
241250 webPreferences : {
242251 nodeIntegration : false ,
243252 contextIsolation : true ,
@@ -248,6 +257,10 @@ function createWindow() {
248257
249258 mainWindow . loadFile ( path . join ( __dirname , '../renderer/index.html' ) ) ;
250259
260+ mainWindow . once ( 'ready-to-show' , ( ) => {
261+ mainWindow ?. show ( ) ;
262+ } ) ;
263+
251264 mainWindow . on ( 'closed' , ( ) => {
252265 mainWindow = null ;
253266 // Close all cached file handlers
@@ -275,6 +288,28 @@ app.on('window-all-closed', () => {
275288 }
276289} ) ;
277290
291+ // === Window Controls ===
292+
293+ ipcMain . handle ( 'window-minimize' , ( ) => {
294+ mainWindow ?. minimize ( ) ;
295+ } ) ;
296+
297+ ipcMain . handle ( 'window-maximize' , ( ) => {
298+ if ( mainWindow ?. isMaximized ( ) ) {
299+ mainWindow . unmaximize ( ) ;
300+ } else {
301+ mainWindow ?. maximize ( ) ;
302+ }
303+ } ) ;
304+
305+ ipcMain . handle ( 'window-close' , ( ) => {
306+ mainWindow ?. close ( ) ;
307+ } ) ;
308+
309+ ipcMain . handle ( 'get-platform' , ( ) => {
310+ return process . platform ;
311+ } ) ;
312+
278313// === File Operations ===
279314
280315ipcMain . handle ( IPC . OPEN_FILE_DIALOG , async ( ) => {
@@ -859,10 +894,8 @@ ipcMain.handle('export-bookmarks', async () => {
859894 . sort ( ( a , b ) => a . lineNumber - b . lineNumber ) ;
860895
861896 for ( const bookmark of sortedBookmarks ) {
862- // Get the line text
863- const [ lineData ] = handler ?. getLines ( bookmark . lineNumber , 1 ) || [ ] ;
864- const lineText = lineData ?. text || '' ;
865- const truncatedText = lineText . length > 100 ? lineText . substring ( 0 , 100 ) + '...' : lineText ;
897+ // Use stored lineText if available, otherwise fetch from file
898+ const lineText = bookmark . lineText || ( handler ?. getLines ( bookmark . lineNumber , 1 ) ?. [ 0 ] ?. text ) || '' ;
866899
867900 lines . push ( `## Line ${ bookmark . lineNumber + 1 } ` ) ;
868901 lines . push ( `` ) ;
@@ -874,7 +907,7 @@ ipcMain.handle('export-bookmarks', async () => {
874907 lines . push ( `**Link:** \`${ fileInfo . path } :${ bookmark . lineNumber + 1 } \`` ) ;
875908 lines . push ( `` ) ;
876909 lines . push ( `\`\`\`` ) ;
877- lines . push ( truncatedText ) ;
910+ lines . push ( lineText ) ;
878911 lines . push ( `\`\`\`` ) ;
879912 lines . push ( `` ) ;
880913 lines . push ( `---` ) ;
@@ -941,6 +974,45 @@ ipcMain.handle('highlight-get-next-color', async () => {
941974 return { success : true , color : getNextColor ( ) } ;
942975} ) ;
943976
977+ // === Highlight Groups ===
978+
979+ function loadHighlightGroups ( ) : HighlightGroup [ ] {
980+ try {
981+ const filePath = getHighlightGroupsPath ( ) ;
982+ if ( fs . existsSync ( filePath ) ) {
983+ return JSON . parse ( fs . readFileSync ( filePath , 'utf-8' ) ) ;
984+ }
985+ } catch { /* ignore */ }
986+ return [ ] ;
987+ }
988+
989+ function saveHighlightGroups ( groups : HighlightGroup [ ] ) : void {
990+ ensureConfigDir ( ) ;
991+ fs . writeFileSync ( getHighlightGroupsPath ( ) , JSON . stringify ( groups , null , 2 ) , 'utf-8' ) ;
992+ }
993+
994+ ipcMain . handle ( 'highlight-group-list' , async ( ) => {
995+ return { success : true , groups : loadHighlightGroups ( ) } ;
996+ } ) ;
997+
998+ ipcMain . handle ( 'highlight-group-save' , async ( _ , group : HighlightGroup ) => {
999+ const groups = loadHighlightGroups ( ) ;
1000+ const existingIdx = groups . findIndex ( g => g . id === group . id ) ;
1001+ if ( existingIdx >= 0 ) {
1002+ groups [ existingIdx ] = group ;
1003+ } else {
1004+ groups . push ( group ) ;
1005+ }
1006+ saveHighlightGroups ( groups ) ;
1007+ return { success : true } ;
1008+ } ) ;
1009+
1010+ ipcMain . handle ( 'highlight-group-delete' , async ( _ , groupId : string ) => {
1011+ const groups = loadHighlightGroups ( ) . filter ( g => g . id !== groupId ) ;
1012+ saveHighlightGroups ( groups ) ;
1013+ return { success : true } ;
1014+ } ) ;
1015+
9441016// === Utility ===
9451017
9461018ipcMain . handle ( 'get-file-info' , async ( ) => {
@@ -984,7 +1056,7 @@ ipcMain.handle('open-external-url', async (_, url: string) => {
9841056
9851057// === Save Selected Lines ===
9861058
987- ipcMain . handle ( 'save-selected-lines' , async ( _ , startLine : number , endLine : number ) => {
1059+ ipcMain . handle ( 'save-selected-lines' , async ( _ , startLine : number , endLine : number , columnConfig ?: ColumnConfig ) => {
9881060 const handler = getFileHandler ( ) ;
9891061 if ( ! handler ) return { success : false , error : 'No file open' } ;
9901062
@@ -1014,8 +1086,8 @@ ipcMain.handle('save-selected-lines', async (_, startLine: number, endLine: numb
10141086 const filename = `${ timestamp } .log` ;
10151087 const filePath = path . join ( selectedDir , filename ) ;
10161088
1017- // Write lines to file
1018- const content = lines . map ( l => l . text ) . join ( '\n' ) ;
1089+ // Write lines to file, respecting column visibility
1090+ const content = lines . map ( l => filterLineToVisibleColumns ( l . text , columnConfig ) ) . join ( '\n' ) ;
10191091 fs . writeFileSync ( filePath , content , 'utf-8' ) ;
10201092
10211093 return { success : true , filePath, lineCount : lines . length } ;
@@ -1088,7 +1160,8 @@ ipcMain.handle('save-to-notes', async (
10881160 startLine : number ,
10891161 endLine : number ,
10901162 note ?: string ,
1091- targetFilePath ?: string // If provided, append to this file; otherwise create new
1163+ targetFilePath ?: string , // If provided, append to this file; otherwise create new
1164+ columnConfig ?: ColumnConfig
10921165) => {
10931166 const handler = getFileHandler ( ) ;
10941167 if ( ! handler ) return { success : false , error : 'No file open' } ;
@@ -1153,7 +1226,7 @@ ipcMain.handle('save-to-notes', async (
11531226 content += [
11541227 '' ,
11551228 `--- [${ timestamp } ] Lines ${ startLine + 1 } -${ endLine + 1 } ${ noteDesc } ---` ,
1156- ...lines . map ( l => l . text ) ,
1229+ ...lines . map ( l => filterLineToVisibleColumns ( l . text , columnConfig ) ) ,
11571230 '' ,
11581231 ] . join ( '\n' ) ;
11591232
@@ -1258,7 +1331,8 @@ interface FilterConfig {
12581331 levels : string [ ] ;
12591332 includePatterns : string [ ] ;
12601333 excludePatterns : string [ ] ;
1261- collapseDuplicates : boolean ;
1334+ matchCase ?: boolean ;
1335+ exactMatch ?: boolean ;
12621336 contextLines ?: number ;
12631337 advancedFilter ?: AdvancedFilterConfig ;
12641338}
@@ -1334,6 +1408,31 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
13341408 ? compileAdvancedFilter ( config . advancedFilter ! )
13351409 : null ;
13361410
1411+ // For basic filter: separate include and exclude passes
1412+ // Include matches get context window, exclude removes exact lines only
1413+ const hasBasicExclude = ! useAdvancedFilter && config . excludePatterns . length > 0 ;
1414+ const excludeLines : Set < number > = new Set ( ) ;
1415+
1416+ // Pattern matching helper respecting matchCase and exactMatch options
1417+ const caseSensitive = config . matchCase || false ;
1418+ const exactMatch = config . exactMatch || false ;
1419+ const matchPattern = ( text : string , pattern : string ) : boolean => {
1420+ if ( exactMatch ) {
1421+ // Literal substring match
1422+ return caseSensitive
1423+ ? text . includes ( pattern )
1424+ : text . toLowerCase ( ) . includes ( pattern . toLowerCase ( ) ) ;
1425+ }
1426+ // Regex match with fallback to substring
1427+ try {
1428+ return new RegExp ( pattern , caseSensitive ? '' : 'i' ) . test ( text ) ;
1429+ } catch {
1430+ return caseSensitive
1431+ ? text . includes ( pattern )
1432+ : text . toLowerCase ( ) . includes ( pattern . toLowerCase ( ) ) ;
1433+ }
1434+ } ;
1435+
13371436 // Process in batches for performance
13381437 const batchSize = 10000 ;
13391438 for ( let start = 0 ; start < totalLines ; start += batchSize ) {
@@ -1357,24 +1456,15 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
13571456
13581457 // Include patterns (OR logic)
13591458 if ( matches && config . includePatterns . length > 0 ) {
1360- matches = config . includePatterns . some ( pattern => {
1361- try {
1362- return new RegExp ( pattern , 'i' ) . test ( line . text ) ;
1363- } catch {
1364- return line . text . toLowerCase ( ) . includes ( pattern . toLowerCase ( ) ) ;
1365- }
1366- } ) ;
1459+ matches = config . includePatterns . some ( pattern => matchPattern ( line . text , pattern ) ) ;
13671460 }
13681461
1369- // Exclude patterns (AND logic - all must not match)
1370- if ( matches && config . excludePatterns . length > 0 ) {
1371- matches = ! config . excludePatterns . some ( pattern => {
1372- try {
1373- return new RegExp ( pattern , 'i' ) . test ( line . text ) ;
1374- } catch {
1375- return line . text . toLowerCase ( ) . includes ( pattern . toLowerCase ( ) ) ;
1376- }
1377- } ) ;
1462+ // Track exclude matches separately (exact lines only)
1463+ if ( hasBasicExclude ) {
1464+ const excluded = config . excludePatterns . some ( pattern => matchPattern ( line . text , pattern ) ) ;
1465+ if ( excluded ) {
1466+ excludeLines . add ( line . lineNumber ) ;
1467+ }
13781468 }
13791469 }
13801470
@@ -1384,7 +1474,7 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
13841474 }
13851475 }
13861476
1387- // Add context lines
1477+ // Add context lines around include matches (before exclude removal)
13881478 if ( contextLines > 0 ) {
13891479 const matchArray = Array . from ( matchingLines ) ;
13901480 for ( const lineNum of matchArray ) {
@@ -1395,6 +1485,13 @@ ipcMain.handle('apply-filter', async (_, config: FilterConfig) => {
13951485 }
13961486 }
13971487
1488+ // Remove exact exclude lines after context expansion
1489+ if ( hasBasicExclude ) {
1490+ for ( const lineNum of excludeLines ) {
1491+ matchingLines . delete ( lineNum ) ;
1492+ }
1493+ }
1494+
13981495 // Sort and store
13991496 const sortedLines = Array . from ( matchingLines ) . sort ( ( a , b ) => a - b ) ;
14001497 filterState . set ( currentFilePath , sortedLines ) ;
0 commit comments