diff --git a/.github/scripts/generate-release-note.js b/.github/scripts/generate-release-note.js index 4674979de..43e4bd838 100644 --- a/.github/scripts/generate-release-note.js +++ b/.github/scripts/generate-release-note.js @@ -46,6 +46,10 @@ const tag = (process.env.GITHUB_REF_NAME && /^v\d+\.\d+\.\d+/.test(process.env.G const baseUrl = `https://github.com/${repo}/releases/download/${tag}`; // Filename patterns based on electron-builder.config.cjs artifactName: '${productName}-${version}-${os}-${arch}.${ext}' +// Note: electron-builder uses different arch names for Linux packages: +// - AppImage: x64 -> x86_64, arm64 -> arm64 +// - deb: x64 -> amd64, arm64 -> arm64 +// - rpm: x64 -> x86_64, arm64 -> aarch64 const files = { mac: { arm64: `Netcatty-${version}-mac-arm64.dmg`, @@ -57,16 +61,16 @@ const files = { }, linux: { appimage: { - x64: `Netcatty-${version}-linux-x64.AppImage`, + x64: `Netcatty-${version}-linux-x86_64.AppImage`, arm64: `Netcatty-${version}-linux-arm64.AppImage` }, deb: { - x64: `Netcatty-${version}-linux-x64.deb`, + x64: `Netcatty-${version}-linux-amd64.deb`, arm64: `Netcatty-${version}-linux-arm64.deb` }, rpm: { - x64: `Netcatty-${version}-linux-x64.rpm`, - arm64: `Netcatty-${version}-linux-arm64.rpm` + x64: `Netcatty-${version}-linux-x86_64.rpm`, + arm64: `Netcatty-${version}-linux-aarch64.rpm` } } }; diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml deleted file mode 100644 index 5dfa5f80e..000000000 --- a/.github/workflows/sync.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Sync Upstream - -env: - UPSTREAM_URL: "https://github.com/binaricat/Netcatty.git" - UPSTREAM_BRANCH: "main" - TARGET_BRANCH: "main" - -on: - schedule: - - cron: '0 0 * * *' # Run daily at midnight - workflow_dispatch: # Allow manual trigger - -jobs: - sync: - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ env.TARGET_BRANCH }} - fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - - - name: Configure Git - run: | - git config --global user.name "github-actions[bot]" - git config --global user.email "github-actions[bot]@users.noreply.github.com" - - - name: Merge Upstream - run: | - echo "Adding upstream remote..." - git remote add upstream ${{ env.UPSTREAM_URL }} - git fetch upstream ${{ env.UPSTREAM_BRANCH }} - - echo "Merging upstream/${{ env.UPSTREAM_BRANCH }} into ${{ env.TARGET_BRANCH }}..." - # This will fail if there are conflicts, which is the desired behavior (notify user via failure) - git merge upstream/${{ env.UPSTREAM_BRANCH }} --no-edit - - echo "Pushing changes..." - git push origin ${{ env.TARGET_BRANCH }} diff --git a/application/i18n/locales/en.ts b/application/i18n/locales/en.ts index aad473fed..a9b13edec 100644 --- a/application/i18n/locales/en.ts +++ b/application/i18n/locales/en.ts @@ -1355,6 +1355,9 @@ const en: Messages = { 'passphrase.unlock': 'Unlock', 'passphrase.unlocking': 'Unlocking...', 'passphrase.skip': 'Skip', + + // Text Editor + 'sftp.editor.wordWrap': 'Word Wrap', }; export default en; diff --git a/application/i18n/locales/zh-CN.ts b/application/i18n/locales/zh-CN.ts index 03ef333b6..d05cf075c 100644 --- a/application/i18n/locales/zh-CN.ts +++ b/application/i18n/locales/zh-CN.ts @@ -1341,6 +1341,9 @@ const zhCN: Messages = { 'passphrase.unlock': '解锁', 'passphrase.unlocking': '解锁中...', 'passphrase.skip': '跳过', + + // Text Editor + 'sftp.editor.wordWrap': '自动换行', }; export default zhCN; diff --git a/application/state/useSettingsState.ts b/application/state/useSettingsState.ts index d7c5dba45..129f8ef1a 100644 --- a/application/state/useSettingsState.ts +++ b/application/state/useSettingsState.ts @@ -1,31 +1,32 @@ -import { useCallback,useEffect,useLayoutEffect,useMemo,useState } from 'react'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react'; import { SyncConfig, TerminalSettings, DEFAULT_TERMINAL_SETTINGS, HotkeyScheme, CustomKeyBindings, DEFAULT_KEY_BINDINGS, KeyBinding, UILanguage, SessionLogFormat } from '../../domain/models'; import { -STORAGE_KEY_COLOR, -STORAGE_KEY_SYNC, -STORAGE_KEY_TERM_THEME, -STORAGE_KEY_THEME, -STORAGE_KEY_TERM_FONT_FAMILY, -STORAGE_KEY_TERM_FONT_SIZE, -STORAGE_KEY_TERM_SETTINGS, -STORAGE_KEY_HOTKEY_SCHEME, -STORAGE_KEY_CUSTOM_KEY_BINDINGS, -STORAGE_KEY_HOTKEY_RECORDING, -STORAGE_KEY_CUSTOM_CSS, -STORAGE_KEY_UI_LANGUAGE, -STORAGE_KEY_ACCENT_MODE, -STORAGE_KEY_UI_THEME_LIGHT, -STORAGE_KEY_UI_THEME_DARK, -STORAGE_KEY_UI_FONT_FAMILY, -STORAGE_KEY_SFTP_DOUBLE_CLICK_BEHAVIOR, -STORAGE_KEY_SFTP_AUTO_SYNC, -STORAGE_KEY_SFTP_SHOW_HIDDEN_FILES, -STORAGE_KEY_SFTP_USE_COMPRESSED_UPLOAD, -STORAGE_KEY_SESSION_LOGS_ENABLED, -STORAGE_KEY_SESSION_LOGS_DIR, -STORAGE_KEY_SESSION_LOGS_FORMAT, -STORAGE_KEY_TOGGLE_WINDOW_HOTKEY, -STORAGE_KEY_CLOSE_TO_TRAY, + STORAGE_KEY_COLOR, + STORAGE_KEY_SYNC, + STORAGE_KEY_TERM_THEME, + STORAGE_KEY_THEME, + STORAGE_KEY_TERM_FONT_FAMILY, + STORAGE_KEY_TERM_FONT_SIZE, + STORAGE_KEY_TERM_SETTINGS, + STORAGE_KEY_HOTKEY_SCHEME, + STORAGE_KEY_CUSTOM_KEY_BINDINGS, + STORAGE_KEY_HOTKEY_RECORDING, + STORAGE_KEY_CUSTOM_CSS, + STORAGE_KEY_UI_LANGUAGE, + STORAGE_KEY_ACCENT_MODE, + STORAGE_KEY_UI_THEME_LIGHT, + STORAGE_KEY_UI_THEME_DARK, + STORAGE_KEY_UI_FONT_FAMILY, + STORAGE_KEY_SFTP_DOUBLE_CLICK_BEHAVIOR, + STORAGE_KEY_SFTP_AUTO_SYNC, + STORAGE_KEY_SFTP_SHOW_HIDDEN_FILES, + STORAGE_KEY_SFTP_USE_COMPRESSED_UPLOAD, + STORAGE_KEY_EDITOR_WORD_WRAP, + STORAGE_KEY_SESSION_LOGS_ENABLED, + STORAGE_KEY_SESSION_LOGS_DIR, + STORAGE_KEY_SESSION_LOGS_FORMAT, + STORAGE_KEY_TOGGLE_WINDOW_HOTKEY, + STORAGE_KEY_CLOSE_TO_TRAY, } from '../../infrastructure/config/storageKeys'; import { DEFAULT_UI_LOCALE, resolveSupportedLocale } from '../../infrastructure/config/i18n'; import { TERMINAL_THEMES } from '../../infrastructure/config/terminalThemes'; @@ -54,6 +55,9 @@ const DEFAULT_SFTP_AUTO_SYNC = false; const DEFAULT_SFTP_SHOW_HIDDEN_FILES = false; const DEFAULT_SFTP_USE_COMPRESSED_UPLOAD = true; +// Editor defaults +const DEFAULT_EDITOR_WORD_WRAP = false; + // Session Logs defaults const DEFAULT_SESSION_LOGS_ENABLED = false; const DEFAULT_SESSION_LOGS_FORMAT: SessionLogFormat = 'txt'; @@ -89,7 +93,7 @@ const isValidUiFontId = (value: string): boolean => { if (value.startsWith('local-')) return true; // Check bundled fonts first, then check dynamically loaded fonts return UI_FONTS.some((font) => font.id === value) || - uiFontStore.getAvailableFonts().some((font) => font.id === value); + uiFontStore.getAvailableFonts().some((font) => font.id === value); }; const applyThemeTokens = ( @@ -208,6 +212,12 @@ export const useSettingsState = () => { return DEFAULT_SFTP_USE_COMPRESSED_UPLOAD; }); + // Editor Settings + const [editorWordWrap, setEditorWordWrapState] = useState(() => { + const stored = readStoredString(STORAGE_KEY_EDITOR_WORD_WRAP); + return stored === 'true' ? true : DEFAULT_EDITOR_WORD_WRAP; + }); + // Session Logs Settings const [sessionLogsEnabled, setSessionLogsEnabled] = useState(() => { const stored = readStoredString(STORAGE_KEY_SESSION_LOGS_ENABLED); @@ -307,11 +317,11 @@ export const useSettingsState = () => { }, [uiFontFamilyId, uiFontsLoaded, notifySettingsChanged]); // Listen for settings changes from other windows via IPC - useEffect(() => { - const bridge = netcattyBridge.get(); - if (!bridge?.onSettingsChanged) return; - const unsubscribe = bridge.onSettingsChanged((payload) => { - const { key, value } = payload; + useEffect(() => { + const bridge = netcattyBridge.get(); + if (!bridge?.onSettingsChanged) return; + const unsubscribe = bridge.onSettingsChanged((payload) => { + const { key, value } = payload; if ( key === STORAGE_KEY_THEME || key === STORAGE_KEY_UI_THEME_LIGHT || @@ -325,8 +335,8 @@ export const useSettingsState = () => { if (key === STORAGE_KEY_UI_LANGUAGE && typeof value === 'string') { const next = resolveSupportedLocale(value); setUiLanguage((prev) => (prev === next ? prev : next)); - document.documentElement.lang = next; - } + document.documentElement.lang = next; + } if (key === STORAGE_KEY_CUSTOM_CSS && typeof value === 'string') { syncCustomCssFromStorage(); } @@ -447,14 +457,14 @@ export const useSettingsState = () => { } } // Sync terminal settings from other windows - if (e.key === STORAGE_KEY_TERM_SETTINGS && e.newValue) { - try { - const newSettings = JSON.parse(e.newValue) as TerminalSettings; - setTerminalSettings(_prev => ({ ...DEFAULT_TERMINAL_SETTINGS, ...newSettings })); - } catch { - // ignore parse errors - } - } + if (e.key === STORAGE_KEY_TERM_SETTINGS && e.newValue) { + try { + const newSettings = JSON.parse(e.newValue) as TerminalSettings; + setTerminalSettings(_prev => ({ ...DEFAULT_TERMINAL_SETTINGS, ...newSettings })); + } catch { + // ignore parse errors + } + } // Sync terminal theme from other windows if (e.key === STORAGE_KEY_TERM_THEME && e.newValue) { if (e.newValue !== terminalThemeId) { @@ -755,6 +765,13 @@ export const useSettingsState = () => { setSftpShowHiddenFiles, sftpUseCompressedUpload, setSftpUseCompressedUpload, + // Editor Settings + editorWordWrap, + setEditorWordWrap: useCallback((enabled: boolean) => { + setEditorWordWrapState(enabled); + localStorageAdapter.writeString(STORAGE_KEY_EDITOR_WORD_WRAP, String(enabled)); + notifySettingsChanged(STORAGE_KEY_EDITOR_WORD_WRAP, enabled); + }, [notifySettingsChanged]), availableFonts, // Session Logs sessionLogsEnabled, diff --git a/components/TextEditorModal.tsx b/components/TextEditorModal.tsx index 02198719d..d97df1031 100644 --- a/components/TextEditorModal.tsx +++ b/components/TextEditorModal.tsx @@ -5,6 +5,7 @@ import { CloudUpload, Loader2, Search, + WrapText, X, } from 'lucide-react'; import Editor, { type OnMount, loader, useMonaco } from '@monaco-editor/react'; @@ -18,6 +19,7 @@ const monacoBasePath = import.meta.env.DEV loader.config({ paths: { vs: monacoBasePath } }); import { useI18n } from '../application/i18n/I18nProvider'; +import { useSettingsState } from '../application/state/useSettingsState'; import { getLanguageId, getLanguageName, getSupportedLanguages } from '../lib/sftpFileUtils'; import { Button } from './ui/button'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'; @@ -134,6 +136,7 @@ export const TextEditorModal: React.FC = ({ onSave, }) => { const { t } = useI18n(); + const { editorWordWrap, setEditorWordWrap } = useSettingsState(); const monaco = useMonaco(); const [content, setContent] = useState(initialContent); const [saving, setSaving] = useState(false); @@ -299,6 +302,17 @@ export const TextEditorModal: React.FC = ({ + {/* Word wrap toggle */} + + {/* Language selector */} = ({ automaticLayout: true, tabSize: 2, insertSpaces: true, - wordWrap: 'off', + wordWrap: editorWordWrap ? 'on' : 'off', folding: true, renderWhitespace: 'selection', bracketPairColorization: { enabled: true }, diff --git a/infrastructure/config/storageKeys.ts b/infrastructure/config/storageKeys.ts index 5826ccc09..f8892e449 100644 --- a/infrastructure/config/storageKeys.ts +++ b/infrastructure/config/storageKeys.ts @@ -47,6 +47,9 @@ export const STORAGE_KEY_SFTP_AUTO_SYNC = 'netcatty_sftp_auto_sync_v1'; export const STORAGE_KEY_SFTP_SHOW_HIDDEN_FILES = 'netcatty_sftp_show_hidden_files_v1'; export const STORAGE_KEY_SFTP_USE_COMPRESSED_UPLOAD = 'netcatty_sftp_use_compressed_upload_v1'; +// Editor Settings +export const STORAGE_KEY_EDITOR_WORD_WRAP = 'netcatty_editor_word_wrap_v1'; + // Session Logs Settings export const STORAGE_KEY_SESSION_LOGS_ENABLED = 'netcatty_session_logs_enabled_v1'; export const STORAGE_KEY_SESSION_LOGS_DIR = 'netcatty_session_logs_dir_v1'; diff --git a/package-lock.json b/package-lock.json index a664b70a6..f6f40449d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -955,13 +955,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.2.tgz", - "integrity": "sha512-jGOOV/bV1DhkkUhHiZ3/1GZ67cZyOXaDb7d1rYD6ZiXf5V9tBNOcgqXwRRPvrCbYaFRa1pPMFb3ZjqjWpR3YfA==", + "version": "3.972.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", + "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.2.5", + "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" }, "engines": { @@ -2544,9 +2544,9 @@ } }, "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", - "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", + "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8239,9 +8239,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", + "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", "funding": [ { "type": "github", @@ -12433,16 +12433,16 @@ } }, "node_modules/webdav": { - "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.8.0.tgz", - "integrity": "sha512-iuFG7NamJ41Oshg4930iQgfIpRrUiatPWIekeznYgEf2EOraTRcDPTjy7gIOMtkdpKTaqPk1E68NO5PAGtJahA==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-5.9.0.tgz", + "integrity": "sha512-OMJ6wtK1WvCO++aOLoQgE96S8KT4e5aaClWHmHXfFU369r4eyELN569B7EqT4OOUb99mmO58GkyuiCv/Ag6J0Q==", "license": "MIT", "dependencies": { "@buttercup/fetch": "^0.2.1", "base-64": "^1.0.0", "byte-length": "^1.0.2", - "entities": "^6.0.0", - "fast-xml-parser": "^4.5.1", + "entities": "^6.0.1", + "fast-xml-parser": "^5.3.4", "hot-patcher": "^2.0.1", "layerr": "^3.0.0", "md5": "^2.3.0", @@ -12457,36 +12457,6 @@ "node": ">=14" } }, - "node_modules/webdav/node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/webdav/node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",