diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f05dcaa..bb944a9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,21 @@ jobs: if: runner.os == 'Windows' shell: pwsh run: | - choco install nsis -y --no-progress + $installed = $false + for ($attempt = 1; $attempt -le 3; $attempt++) { + choco install nsis -y --no-progress + if ($LASTEXITCODE -eq 0) { + $installed = $true + break + } + if ($attempt -lt 3) { + Write-Host "NSIS installation attempt $attempt failed; retrying after 15 seconds." + Start-Sleep -Seconds 15 + } + } + if (-not $installed) { + throw 'NSIS installation failed after 3 attempts.' + } $env:Path = "C:\Program Files (x86)\NSIS;$env:Path" if (-not (Get-Command makensis.exe -ErrorAction SilentlyContinue)) { throw 'NSIS compiler was not installed.' diff --git a/src/features/ui/platform/macos/ui_macos.m b/src/features/ui/platform/macos/ui_macos.m index efd374a..a3057e6 100644 --- a/src/features/ui/platform/macos/ui_macos.m +++ b/src/features/ui/platform/macos/ui_macos.m @@ -1,4 +1,5 @@ #include "ui_macos_internal.h" +#include "axyne/memory_usage.h" /* Menu text at the Figma 12pt size. Only the font is set so AppKit keeps * choosing the colour (which also dims disabled entries) and draws the @@ -4887,11 +4888,22 @@ - (BOOL)requireEditorFor:(NSString *)action - (void)viewDidMoveToWindow { [super viewDidMoveToWindow]; + [_statusTimer invalidate]; + _statusTimer = nil; + if ([self window] != nil) + _statusTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self + selector:@selector(statusTimerTick:) userInfo:nil repeats:YES]; [self loadScintillaView]; [self applyPreferences]; [self loadActiveDocument]; } +- (void)statusTimerTick:(NSTimer *)timer +{ + (void)timer; + [self setNeedsDisplay:YES]; +} + /* A click on the menu bar while the window is inactive opens the menu * straight away instead of only activating the window. */ - (BOOL)acceptsFirstMouse:(NSEvent *)event @@ -6955,6 +6967,85 @@ - (void)drawDefaultStatusAtTop:(CGFloat)statusTop width:(CGFloat)width [self drawLabel:editing at:NSMakePoint(MAX(180, width - 320), statusTop + 5) size:11 color:text family:@"SF Pro Text"]; } + { + AxyneDocument *document = [self isEmptyState] ? NULL : [self activeDocument]; + NSInteger line = 1, column = 1, eol = SC_EOL_LF, codepage = SC_CP_UTF8; + AxyneLanguageId language = document != NULL && document->path != NULL + ? axyne_language_for_path(document->path) : AXYNE_LANGUAGE_NONE; + const char *languageName = language != AXYNE_LANGUAGE_NONE + ? axyne_language_name(language) : "Plain Text"; + const char *toolchain = _buildTarget.platform == AXYNE_PLATFORM_WINDOWS ? "MSVC" : "Clang"; + char target[96]; + AxyneMemoryUsage memory; + char gauge[64]; + NSString *encoding; + NSString *eolName; + NSString *metadata; + NSFont *statusFont = [NSFont fontWithName:@"SF Pro Text" size:11]; + NSDictionary *statusAttributes; + CGFloat memoryWidth, metadataWidth, barWidth = 96; + CGFloat cursorX; + CGFloat fillWidth; + + if (document != NULL && _editorView != nil) { + NSInteger caret = [self sendEditorMessage:SCI_GETCURRENTPOS wParam:0 lParam:0]; + line = [self sendEditorMessage:SCI_LINEFROMPOSITION wParam:caret lParam:0] + 1; + column = [self sendEditorMessage:SCI_GETCOLUMN wParam:caret lParam:0] + 1; + eol = [self sendEditorMessage:SCI_GETEOLMODE wParam:0 lParam:0]; + codepage = [self sendEditorMessage:SCI_GETCODEPAGE wParam:0 lParam:0]; + } + if (_runtimesDiscovered && language != AXYNE_LANGUAGE_NONE && document != NULL && + document->path != NULL) { + AxyneLanguagePlan plan; + char message[256]; + memset(&plan, 0, sizeof(plan)); + if (axyne_language_resolve_runner(language, &_runtimes, &_buildTarget, + document->path, NULL, &plan, message, sizeof(message)) == AXYNE_STATUS_OK && + plan.toolchain_name != NULL && plan.toolchain_name[0] != '\0') + toolchain = plan.toolchain_name; + axyne_language_plan_free(&plan); + } + (void)snprintf(target, sizeof(target), "%s %s %s", toolchain, + axyne_architecture_name(_buildTarget.platform, _buildTarget.architecture), + axyne_configuration_name(_buildTarget.configuration)); + encoding = codepage == SC_CP_UTF8 ? @"UTF-8" : + [NSString stringWithFormat:@"CP%ld", (long)codepage]; + eolName = eol == SC_EOL_CRLF ? @"CRLF" : (eol == SC_EOL_CR ? @"CR" : @"LF"); + if (axyne_memory_usage_sample(&memory, NULL) != AXYNE_STATUS_OK) + memset(&memory, 0, sizeof(memory)); + (void)axyne_memory_format_gauge(memory.resident_bytes, + AXYNE_MEMORY_BUDGET_BYTES, gauge, sizeof(gauge)); + metadata = document != NULL + ? [NSString stringWithFormat:@"줄 %ld, 열 %ld %@: %u %@ %@ %s %s", + (long)line, (long)column, + _preferences.editor.insert_spaces ? @"공백" : @"탭", + _preferences.editor.tab_width, encoding, eolName, languageName, target] + : @""; + statusAttributes = @{NSFontAttributeName: statusFont != nil ? statusFont : [NSFont systemFontOfSize:11]}; + memoryWidth = [[NSString stringWithUTF8String:gauge] sizeWithAttributes:statusAttributes].width; + cursorX = width - 12 - barWidth; + metadata = [self label:metadata fittingWidth:MAX(0, + cursorX - 10 - memoryWidth - 18 - 180) + size:11 family:@"SF Pro Text"]; + metadataWidth = [metadata sizeWithAttributes:statusAttributes].width; + [axyne_preference_color(_preferences.theme.toolbar) setFill]; + NSRectFill(NSMakeRect(180, statusTop, MAX(0, width - 180), AXYNE_STATUS)); + [[axyne_preference_color(0x343942) colorWithAlphaComponent:1.0] setFill]; + NSRectFill(NSMakeRect(cursorX, statusTop + 8, barWidth, 8)); + fillWidth = MIN(barWidth, barWidth * (CGFloat)memory.resident_bytes / + (CGFloat)AXYNE_MEMORY_BUDGET_BYTES); + if (fillWidth > 0) { + [axyne_preference_color(axyne_memory_usage_level(memory.resident_bytes) == + AXYNE_MEMORY_LEVEL_NORMAL ? 0xa66bf0 : 0xe5a445) setFill]; + NSRectFill(NSMakeRect(cursorX, statusTop + 8, fillWidth, 8)); + } + cursorX -= 10 + memoryWidth; + [self drawLabel:[NSString stringWithUTF8String:gauge] + at:NSMakePoint(cursorX, statusTop + 5) size:11 color:text family:@"SF Pro Text"]; + cursorX -= 18 + metadataWidth; + [self drawLabel:metadata at:NSMakePoint(MAX(180, cursorX), statusTop + 5) + size:11 color:text family:@"SF Pro Text"]; + } } - (AxyneMacChromeColors)chromeColors @@ -6994,6 +7085,8 @@ - (void)notifyDocumentWillClose:(AxyneDocument *)document } - (void)dealloc { + [_statusTimer invalidate]; + _statusTimer = nil; #define AXYNE_MAC_HOOK(feature) [self feature##Teardown]; AXYNE_MAC_FEATURES(AXYNE_MAC_HOOK) #undef AXYNE_MAC_HOOK diff --git a/src/features/ui/platform/macos/ui_macos_internal.h b/src/features/ui/platform/macos/ui_macos_internal.h index 3011da7..a42af52 100644 --- a/src/features/ui/platform/macos/ui_macos_internal.h +++ b/src/features/ui/platform/macos/ui_macos_internal.h @@ -303,6 +303,7 @@ typedef struct AxyneDiscoveryBox { id target; } AxyneDiscoveryBox; AxynePaletteController _palette; AxynePaletteOverlay *_paletteOverlay; NSTextField *_paletteField; + NSTimer *_statusTimer; NSTimer *_paletteTimer; /* Feature category objects (ui__macos.m). Each starts nil, is * created lazily by its category and released by its Teardown hook; the diff --git a/src/features/ui/platform/windows/ui_windows.c b/src/features/ui/platform/windows/ui_windows.c index ec16363..3f8c4f5 100644 --- a/src/features/ui/platform/windows/ui_windows.c +++ b/src/features/ui/platform/windows/ui_windows.c @@ -13,6 +13,7 @@ #include "axyne/ui.h" #include "axyne/app_paths.h" +#include "axyne/memory_usage.h" #include "terminal_view_windows.h" #include "axyne/ui_design.h" #include "axyne/layout_metrics.h" @@ -69,7 +70,8 @@ enum { AXYNE_DEBUG_BREAKPOINT, AXYNE_PROBLEMS_FILTER = 5040, /* problems panel filter EDIT control id */ AXYNE_PROBLEMS_TIMER = 5121, /* coalesced problems list rebuild */ - AXYNE_OUTLINE_TIMER = 5122 /* debounced outline rescan after edits */ + AXYNE_OUTLINE_TIMER = 5122, /* debounced outline rescan after edits */ + AXYNE_STATUS_TIMER = 5124 /* refresh process memory in the status bar */ }; static const wchar_t AXYNE_WINDOW_CLASS[] = L"AxyneWindow"; @@ -8819,6 +8821,7 @@ static void axyne_paint_empty_guide(HDC dc, AxyneWindowState *state, int left, static void axyne_paint_status(HDC dc, AxyneWindowState *state, int status_top, int width, int height) { + int status_metadata_left = max(axyne_sidebar_size(state) + 8, width - 620); { wchar_t status[96]; if (state->last_exit_failed) { @@ -8841,7 +8844,8 @@ static void axyne_paint_status(HDC dc, AxyneWindowState *state, int status_top, wchar_t *lsp_status = axyne_wide(state->status_text[0] != '\0' ? state->status_text : state->lsp_status); if (lsp_status != NULL) { - RECT rect = {axyne_sidebar_size(state) + 8, status_top, width - 250, height}; + RECT rect = {axyne_sidebar_size(state) + 8, status_top, + max(axyne_sidebar_size(state) + 8, status_metadata_left - 8), height}; axyne_text_rect(dc, state->ui_font, AXYNE_MUTED, rect, lsp_status, DT_LEFT); free(lsp_status); } @@ -8856,6 +8860,97 @@ static void axyne_paint_status(HDC dc, AxyneWindowState *state, int status_top, (long long)line + 1, (long long)column + 1); axyne_text_rect(dc, state->ui_font, AXYNE_MUTED, rect, position, DT_RIGHT); } + { + AxyneDocument *document = axyne_active_visible(state); + AxyneLanguageId language = document != NULL && document->path != NULL + ? axyne_language_for_path(document->path) : AXYNE_LANGUAGE_NONE; + const char *language_name = language != AXYNE_LANGUAGE_NONE + ? axyne_language_name(language) : "Plain Text"; + const char *toolchain = "MSVC"; + char target[96], gauge[64]; + wchar_t metadata[384], gauge_wide[96], language_wide[64]; + wchar_t target_wide[96], encoding[32]; + AxyneMemoryUsage memory; + LRESULT line = 0, column = 0, eol = SC_EOL_LF, codepage = SC_CP_UTF8; + const wchar_t *eol_name = L"LF"; + int bar_width = 96; + int bar_left = width - 12 - bar_width; + int metadata_right; + int metadata_left; + RECT rect; + + if (state->editor != NULL && document != NULL) { + LRESULT caret = SendMessageA(state->editor, SCI_GETCURRENTPOS, 0, 0); + line = SendMessageA(state->editor, SCI_LINEFROMPOSITION, (WPARAM)caret, 0); + column = SendMessageA(state->editor, SCI_GETCOLUMN, (WPARAM)caret, 0); + eol = SendMessageA(state->editor, SCI_GETEOLMODE, 0, 0); + codepage = SendMessageA(state->editor, SCI_GETCODEPAGE, 0, 0); + } + if (eol == SC_EOL_CRLF) eol_name = L"CRLF"; + else if (eol == SC_EOL_CR) eol_name = L"CR"; + if (state->runtimes_discovered && language != AXYNE_LANGUAGE_NONE && + document != NULL && document->path != NULL) { + AxyneLanguagePlan plan; + char message[256]; + memset(&plan, 0, sizeof(plan)); + if (axyne_language_resolve_runner(language, &state->runtimes, + &state->build_target, document->path, NULL, &plan, + message, sizeof(message)) == AXYNE_STATUS_OK && + plan.toolchain_name != NULL && plan.toolchain_name[0] != '\0') + toolchain = plan.toolchain_name; + axyne_language_plan_free(&plan); + } + (void)snprintf(target, sizeof(target), "%s %s %s", toolchain, + axyne_architecture_name(state->build_target.platform, + state->build_target.architecture), + axyne_configuration_name(state->build_target.configuration)); + memset(&memory, 0, sizeof(memory)); + (void)axyne_memory_usage_sample(&memory, NULL); + (void)axyne_memory_format_gauge(memory.resident_bytes, + AXYNE_MEMORY_BUDGET_BYTES, gauge, sizeof(gauge)); + (void)swprintf_s(language_wide, 64, L"%hs", language_name); + (void)swprintf_s(target_wide, 96, L"%hs", target); + (void)swprintf_s(gauge_wide, 96, L"%hs", gauge); + if (codepage == SC_CP_UTF8) + (void)swprintf_s(encoding, 32, L"UTF-8"); + else + (void)swprintf_s(encoding, 32, L"CP%lld", (long long)codepage); + if (document != NULL) { + (void)swprintf_s(metadata, 384, + L"줄 %lld, 열 %lld %s: %u %s %s %s %s", + (long long)line + 1, (long long)column + 1, + state->preferences.editor.insert_spaces ? L"공백" : L"탭", + state->preferences.editor.tab_width, encoding, + eol_name, language_wide, target_wide); + } else { + metadata[0] = L'\0'; + } + axyne_fill(dc, status_metadata_left, status_top, width, height, AXYNE_STATUS_BG); + metadata_right = bar_left - 16 - axyne_measure_text(state->font_small, gauge_wide); + metadata_left = max(status_metadata_left, + metadata_right - axyne_measure_text(state->font_small, metadata) - 4); + rect.left = metadata_left; + rect.top = status_top; + rect.right = metadata_right; + rect.bottom = height; + axyne_text_rect(dc, state->font_small, AXYNE_TEXT, rect, metadata, DT_RIGHT | DT_END_ELLIPSIS); + rect.left = metadata_right + 4; + rect.right = bar_left - 4; + axyne_text_rect(dc, state->font_small, AXYNE_TEXT, rect, gauge_wide, DT_RIGHT); + axyne_round_fill(dc, bar_left, status_top + 8, width - 12, status_top + 16, + 4, axyne_blend(AXYNE_STATUS_BG, AXYNE_TEXT, 20), + axyne_blend(AXYNE_STATUS_BG, AXYNE_TEXT, 20)); + { + int fill = (int)((double)bar_width * (double)memory.resident_bytes / + (double)AXYNE_MEMORY_BUDGET_BYTES); + COLORREF fill_color = axyne_memory_usage_level(memory.resident_bytes) == + AXYNE_MEMORY_LEVEL_NORMAL ? AXYNE_ACCENT : AXYNE_RUN_TEXT; + if (fill > bar_width) fill = bar_width; + if (fill > 0) + axyne_round_fill(dc, bar_left, status_top + 8, bar_left + fill, + status_top + 16, 4, fill_color, fill_color); + } + } } static void axyne_paint_shell(HWND window, AxyneWindowState *state) @@ -9805,6 +9900,7 @@ static LRESULT CALLBACK axyne_window_proc(HWND window, UINT message, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_MODERN, L"Cascadia Mono"); state->main_window = window; + (void)SetTimer(window, AXYNE_STATUS_TIMER, 2000, NULL); state->problem_selected = -1; axyne_outline_init(&state->outline); axyne_open_scintilla(state, window, instance); @@ -9894,6 +9990,10 @@ static LRESULT CALLBACK axyne_window_proc(HWND window, UINT message, break; case WM_TIMER: if (state != NULL && axyne_win_hooks_timer(state, (UINT_PTR)w_param)) return 0; + if (state != NULL && w_param == AXYNE_STATUS_TIMER) { + InvalidateRect(window, NULL, FALSE); + return 0; + } if (state != NULL && w_param == AXYNE_WIN_CHORD_TIMER_ID) { axyne_win_chord_timer(state); return 0; @@ -10540,6 +10640,7 @@ static LRESULT CALLBACK axyne_window_proc(HWND window, UINT message, /* WM_PAINT covers the complete client area. */ return 1; case WM_DESTROY: + KillTimer(window, AXYNE_STATUS_TIMER); if (state != NULL) axyne_cancel_outline_timer(state); /* Child controls still exist during the parent's WM_DESTROY. Their * handles are already invalid by WM_NCDESTROY, so release every tab's