Merge branch 'master' into master

This commit is contained in:
Ben
2024-12-29 13:54:11 +10:00
committed by GitHub
13 changed files with 431 additions and 307 deletions

1
package-lock.json generated
View File

@@ -25,6 +25,7 @@
}
},
"../../../../usr/share/astal/gjs": {
"name": "astal",
"license": "LGPL-2.1"
},
"node_modules/@eslint-community/eslint-utils": {

View File

@@ -8,7 +8,13 @@ export const BarGeneral = (): JSX.Element => {
<scrollable name={'General'} className="bar-theme-page paged-container" vscroll={Gtk.PolicyType.AUTOMATIC}>
<box vertical>
<Header title="General Settings" />
<Option opt={options.theme.font.name} title="Font" type="font" />
<Option
opt={options.theme.font.name}
fontLabel={options.theme.font.label}
fontStyle={options.theme.font.style}
title="Font"
type="font"
/>
<Option opt={options.theme.font.size} title="Font Size" type="string" />
<Option
opt={options.theme.font.weight}

View File

@@ -100,19 +100,6 @@ export const filterConfigForNonTheme = (config: Config): Config => {
* @param themeOnly - A flag indicating whether to save only theme-related properties.
*/
export const saveFileDialog = (filePath: string, themeOnly: boolean): void => {
const original_file_path = filePath;
const file = Gio.File.new_for_path(original_file_path);
const [success, content] = file.load_contents(null);
if (!success) {
console.error(`Could not find 'config.json' at ${TMP}`);
return;
}
const jsonString = new TextDecoder('utf-8').decode(content);
const jsonObject = JSON.parse(jsonString);
const filterHexColorPairs = (jsonObject: Config): Config => {
const filteredObject: Config = {};
@@ -145,9 +132,6 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => {
return filteredObject;
};
const filteredJsonObject = themeOnly ? filterHexColorPairs(jsonObject) : filterOutHexColorPairs(jsonObject);
const filteredContent = JSON.stringify(filteredJsonObject, null, 2);
const dialog = new Gtk.FileChooserDialog({
title: `Save Hyprpanel ${themeOnly ? 'Theme' : 'Config'}`,
action: Gtk.FileChooserAction.SAVE,
@@ -160,55 +144,82 @@ export const saveFileDialog = (filePath: string, themeOnly: boolean): void => {
const response = dialog.run();
if (response === Gtk.ResponseType.ACCEPT) {
const file_path = dialog.get_filename();
console.info(`Original file path: ${file_path}`);
try {
const original_file_path = filePath;
const getIncrementedFilePath = (filePath: string): string => {
let increment = 1;
const baseName = filePath.replace(/(\.\w+)$/, '');
const match = filePath.match(/(\.\w+)$/);
const extension = match ? match[0] : '';
const file = Gio.File.new_for_path(original_file_path);
const [success, content] = file.load_contents(null);
let newFilePath = filePath;
let file = Gio.File.new_for_path(newFilePath);
if (!success) {
console.error(`Could not find 'config.json' at ${TMP}`);
return;
}
while (file.query_exists(null)) {
newFilePath = `${baseName}_${increment}${extension}`;
file = Gio.File.new_for_path(newFilePath);
increment++;
}
const jsonString = new TextDecoder('utf-8').decode(content);
const jsonObject = JSON.parse(jsonString);
return newFilePath;
};
const filteredJsonObject = themeOnly ? filterHexColorPairs(jsonObject) : filterOutHexColorPairs(jsonObject);
const filteredContent = JSON.stringify(filteredJsonObject, null, 2);
const finalFilePath = getIncrementedFilePath(file_path as string);
console.info(`File will be saved at: ${finalFilePath}`);
if (response === Gtk.ResponseType.ACCEPT) {
const file_path = dialog.get_filename();
console.info(`Original file path: ${file_path}`);
try {
const save_file = Gio.File.new_for_path(finalFilePath);
const outputStream = save_file.replace(null, false, Gio.FileCreateFlags.NONE, null);
const dataOutputStream = new Gio.DataOutputStream({
base_stream: outputStream,
});
const getIncrementedFilePath = (filePath: string): string => {
let increment = 1;
const baseName = filePath.replace(/(\.\w+)$/, '');
const match = filePath.match(/(\.\w+)$/);
const extension = match ? match[0] : '';
dataOutputStream.put_string(filteredContent, null);
let newFilePath = filePath;
let file = Gio.File.new_for_path(newFilePath);
dataOutputStream.close(null);
while (file.query_exists(null)) {
newFilePath = `${baseName}_${increment}${extension}`;
file = Gio.File.new_for_path(newFilePath);
increment++;
}
Notify({
summary: 'File Saved Successfully',
body: `At ${finalFilePath}.`,
iconName: icons.ui.info,
});
} catch (e) {
if (e instanceof Error) {
console.error('Failed to write to file:', e.message);
return newFilePath;
};
const finalFilePath = getIncrementedFilePath(file_path as string);
console.info(`File will be saved at: ${finalFilePath}`);
try {
const save_file = Gio.File.new_for_path(finalFilePath);
const outputStream = save_file.replace(null, false, Gio.FileCreateFlags.NONE, null);
const dataOutputStream = new Gio.DataOutputStream({
base_stream: outputStream,
});
dataOutputStream.put_string(filteredContent, null);
dataOutputStream.close(null);
Notify({
summary: 'File Saved Successfully',
body: `At ${finalFilePath}.`,
iconName: icons.ui.info,
});
} catch (e) {
if (e instanceof Error) {
console.error('Failed to write to file:', e.message);
}
}
}
}
dialog.destroy();
dialog.destroy();
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
dialog.destroy();
Notify({
summary: `${themeOnly ? 'Theme' : 'Config'} Export Failed`,
body: errorMessage ?? 'An unknown error occurred.',
iconName: icons.ui.warning,
});
}
};
/**
@@ -231,63 +242,74 @@ export const importFiles = (themeOnly: boolean = false): void => {
const response = dialog.run();
if (response === Gtk.ResponseType.CANCEL) {
dialog.destroy();
return;
}
if (response === Gtk.ResponseType.ACCEPT) {
const filePath: string | null = dialog.get_filename();
if (filePath === null) {
Notify({
summary: 'Failed to import',
body: 'No file selected.',
iconName: icons.ui.warning,
});
return;
}
const importedConfig = loadJsonFile(filePath);
if (!importedConfig) {
try {
if (response === Gtk.ResponseType.CANCEL) {
dialog.destroy();
return;
}
if (response === Gtk.ResponseType.ACCEPT) {
const filePath: string | null = dialog.get_filename();
if (filePath === null) {
Notify({
summary: 'Failed to import',
body: 'No file selected.',
iconName: icons.ui.warning,
});
return;
}
const importedConfig = loadJsonFile(filePath);
if (!importedConfig) {
dialog.destroy();
return;
}
Notify({
summary: `Importing ${themeOnly ? 'Theme' : 'Config'}`,
body: `Importing: ${filePath}`,
iconName: icons.ui.info,
});
const tmpConfigFile = Gio.File.new_for_path(`${TMP}/config.json`);
const optionsConfigFile = Gio.File.new_for_path(CONFIG);
const [tmpSuccess, tmpContent] = tmpConfigFile.load_contents(null);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
if (!tmpSuccess || !optionsSuccess) {
console.error('Failed to read existing configuration files.');
dialog.destroy();
return;
}
let tmpConfig = JSON.parse(new TextDecoder('utf-8').decode(tmpContent));
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
if (themeOnly) {
const filteredConfig = filterConfigForThemeOnly(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
} else {
const filteredConfig = filterConfigForNonTheme(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
}
saveConfigToFile(tmpConfig, `${TMP}/config.json`);
saveConfigToFile(optionsConfig, CONFIG);
}
dialog.destroy();
bash(restartCommand.get());
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
dialog.destroy();
Notify({
summary: `Importing ${themeOnly ? 'Theme' : 'Config'}`,
body: `Importing: ${filePath}`,
iconName: icons.ui.info,
summary: `${themeOnly ? 'Theme' : 'Config'} Import Failed`,
body: errorMessage ?? 'An unknown error occurred.',
iconName: icons.ui.warning,
});
const tmpConfigFile = Gio.File.new_for_path(`${TMP}/config.json`);
const optionsConfigFile = Gio.File.new_for_path(CONFIG);
const [tmpSuccess, tmpContent] = tmpConfigFile.load_contents(null);
const [optionsSuccess, optionsContent] = optionsConfigFile.load_contents(null);
if (!tmpSuccess || !optionsSuccess) {
console.error('Failed to read existing configuration files.');
dialog.destroy();
return;
}
let tmpConfig = JSON.parse(new TextDecoder('utf-8').decode(tmpContent));
let optionsConfig = JSON.parse(new TextDecoder('utf-8').decode(optionsContent));
if (themeOnly) {
const filteredConfig = filterConfigForThemeOnly(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
} else {
const filteredConfig = filterConfigForNonTheme(importedConfig);
tmpConfig = { ...tmpConfig, ...filteredConfig };
optionsConfig = { ...optionsConfig, ...filteredConfig };
}
saveConfigToFile(tmpConfig, `${TMP}/config.json`);
saveConfigToFile(optionsConfig, CONFIG);
}
dialog.destroy();
bash(restartCommand.get());
};

View File

@@ -14,6 +14,8 @@ import { Gtk } from 'astal/gtk3';
const InputField = <T extends string | number | boolean | object>({
opt,
fontStyle,
fontLabel,
type = typeof opt.get() as RowProps<T>['type'],
enums = [],
disabledBinding,
@@ -44,7 +46,7 @@ const InputField = <T extends string | number | boolean | object>({
case 'wallpaper':
return <WallpaperInputter opt={opt} />;
case 'font':
return <FontInputter opt={opt} />;
return <FontInputter fontFamily={opt} fontLabel={fontLabel} fontStyle={fontStyle} />;
case 'color':
return <ColorInputter opt={opt} />;
@@ -55,6 +57,8 @@ const InputField = <T extends string | number | boolean | object>({
export const Inputter = <T extends string | number | boolean | object>({
opt,
fontStyle,
fontLabel,
type = typeof opt.get() as RowProps<T>['type'],
enums,
disabledBinding,
@@ -71,6 +75,8 @@ export const Inputter = <T extends string | number | boolean | object>({
<InputField
type={type}
opt={opt}
fontStyle={fontStyle}
fontLabel={fontLabel}
enums={enums}
disabledBinding={disabledBinding}
dependencies={dependencies}

View File

@@ -10,6 +10,8 @@ export const SettingInput = <T extends string | number | boolean | object>({
return (
<Inputter
opt={props.opt}
fontStyle={props.fontStyle}
fontLabel={props.fontLabel}
type={props.type}
enums={props.enums}
disabledBinding={props.disabledBinding}

View File

@@ -1,23 +0,0 @@
import FontButton from 'src/components/shared/FontButton';
import { Opt } from 'src/lib/option';
export const FontInputter = <T extends string | number | boolean | object>({
opt,
}: FontInputterProps<T>): JSX.Element => {
return (
<FontButton
showSize={false}
useSize={false}
setup={(self) => {
self.font = opt.get() as string;
self.hook(opt, () => (self.font = opt.get() as string));
self.connect('font-set', ({ font }) => opt.set(font!.split(' ').slice(0, -1).join(' ') as T));
}}
/>
);
};
interface FontInputterProps<T> {
opt: Opt<T>;
}

View File

@@ -0,0 +1,43 @@
import FontButton from 'src/components/shared/FontButton';
import { Opt } from 'src/lib/option';
import { styleToString } from './utils';
export const FontInputter = <T extends string | number | boolean | object>({
fontFamily,
fontStyle,
fontLabel,
}: FontInputterProps<T>): JSX.Element => (
<FontButton
showSize={false}
useSize={false}
setup={(self) => {
self.font = fontLabel?.get() ?? (fontFamily.get() as string);
if (fontLabel) {
self.hook(fontLabel, () => {
self.font = fontLabel.get() as string;
});
} else {
self.hook(fontFamily, () => {
self.font = fontFamily.get() as string;
});
}
self.connect('font-set', ({ fontDesc, font }) => {
const selectedFontFamily = fontDesc.get_family();
const selectedFontStyle = styleToString(fontDesc.get_style());
fontFamily.set(selectedFontFamily as T);
fontStyle?.set(selectedFontStyle);
fontLabel?.set(font.split(' ').slice(0, -1).join(' '));
});
}}
/>
);
interface FontInputterProps<T> {
fontFamily: Opt<T>;
fontStyle?: Opt<string>;
fontLabel?: Opt<string>;
}

View File

@@ -0,0 +1,72 @@
import Pango from 'gi://Pango?version=1.0';
export type FontStyle = 'normal' | 'italic' | 'oblique';
export type FontVariant = 'normal' | 'small-caps';
export type FontWeight =
| 'thin'
| 'ultralight'
| 'light'
| 'semilight'
| 'book'
| 'normal'
| 'medium'
| 'semibold'
| 'bold'
| 'ultrabold'
| 'heavy';
const DEFAULT_FONT_STYLE: FontStyle = 'normal';
const DEFAULT_FONT_VARIANT: FontVariant = 'normal';
const DEFAULT_FONT_WEIGHT: FontWeight = 'normal';
const styleMap: Record<Pango.Style, FontStyle> = {
[Pango.Style.NORMAL]: 'normal',
[Pango.Style.ITALIC]: 'italic',
[Pango.Style.OBLIQUE]: 'oblique',
};
const variantMap: Record<Pango.Variant, FontVariant> = {
[Pango.Variant.NORMAL]: 'normal',
[Pango.Variant.SMALL_CAPS]: 'small-caps',
};
const weightMap: Record<Pango.Weight, FontWeight> = {
[Pango.Weight.THIN]: 'thin',
[Pango.Weight.ULTRALIGHT]: 'ultralight',
[Pango.Weight.LIGHT]: 'light',
[Pango.Weight.SEMILIGHT]: 'semilight',
[Pango.Weight.BOOK]: 'book',
[Pango.Weight.NORMAL]: 'normal',
[Pango.Weight.MEDIUM]: 'medium',
[Pango.Weight.SEMIBOLD]: 'semibold',
[Pango.Weight.BOLD]: 'bold',
[Pango.Weight.ULTRABOLD]: 'ultrabold',
[Pango.Weight.HEAVY]: 'heavy',
};
/**
* Converts a Pango.Style enum to a FontStyle string.
* @param styleEnum - The Pango.Style enum value.
* @returns The corresponding FontStyle string.
*/
export function styleToString(styleEnum: Pango.Style): FontStyle {
return styleMap[styleEnum] ?? DEFAULT_FONT_STYLE;
}
/**
* Converts a Pango.Variant enum to a FontVariant string.
* @param variantEnum - The Pango.Variant enum value.
* @returns The corresponding FontVariant string.
*/
export function variantToString(variantEnum: Pango.Variant): FontVariant {
return variantMap[variantEnum] ?? DEFAULT_FONT_VARIANT;
}
/**
* Converts a Pango.Weight enum to a FontWeight string.
* @param weightEnum - The Pango.Weight enum value.
* @returns The corresponding FontWeight string.
*/
export function weightToString(weightEnum: Pango.Weight): FontWeight {
return weightMap[weightEnum] ?? DEFAULT_FONT_WEIGHT;
}

View File

@@ -104,6 +104,8 @@ export interface RowProps<T> {
subtitleLink?: string;
dependencies?: string[];
increment?: number;
fontStyle?: Opt<string>;
fontLabel?: Opt<string>;
}
export type OSDOrientation = 'horizontal' | 'vertical';

View File

@@ -1,3 +1,4 @@
import { FontStyle } from './components/settings/shared/inputs/font/utils';
import { opt, mkOptions } from './lib/option';
import { NetstatLabelType, RateUnit, ResourceLabelType } from './lib/types/bar';
import { KbLabelType } from './lib/types/customModules/kbLayout';
@@ -104,6 +105,8 @@ const options = mkOptions(CONFIG, {
font: {
size: opt('1.2rem'),
name: opt('Ubuntu Nerd Font'),
style: opt<FontStyle>('normal'),
label: opt('Ubuntu Nerd Font'),
weight: opt(600),
},
notification: {

View File

@@ -1,6 +1,7 @@
* {
all: unset;
font-family: $font-name;
font-style: $font-style;
font-size: $font-size;
font-weight: $font-weight;
}

View File

@@ -1,222 +1,210 @@
@import "colors";
@import 'colors';
* {
color: $default_fg;
font-family: "JetBrainsMono NF";
font-size: 0.9rem;
html,
body {
padding: 0;
margin: 0;
background-color: $primary_bg;
}
html, body {
padding: 0;
margin: 0;
background-color: $primary_bg;
}
// scrollbar {
// background-color: red;
// min-width: 5em;
// }
.code {
background: $light-background;
border-radius: 0.5rem;
.code-header {
background: $light-background;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border-bottom: 1px solid $primary_fg;
padding: 5px;
border-radius: 0.5rem;
> button {
color: $default_fg;
background: transparent;
float: right;
border: none;
.code-header {
background: $light-background;
border-top-left-radius: 0.5rem;
border-top-right-radius: 0.5rem;
border-bottom: 1px solid $primary_fg;
padding: 5px;
&:before {
content: '󰆏';
display: inline-block;
padding-right: 0.5rem;
}
> button {
color: $default_fg;
background: transparent;
float: right;
border: none;
&:before {
content: '󰆏';
display: inline-block;
padding-right: 0.5rem;
}
}
}
}
}
$languages-map: (
arduino: "",
armasm: "",
avrasm: "",
bash: "",
c: "",
clojure: "",
coffeescript: "",
cpp: "",
csharp: "󰌛",
css: "",
dockerfile: "󰡨",
go: "",
gradle: "",
haskell: "",
html: "",
java: "",
javascript: "󰌞",
json: "",
latex: "",
lua: "󰢱",
makefile: "",
markdown: "",
mipsasm: "",
nginx: "",
nix: "󱄅",
php: "",
prolog: "",
python: "",
r: "󰟔",
ruby: "",
rust: "",
scss: "",
shell: "",
typescript: "󰛦",
wasm: "",
x86asm: "",
xml: "󰗀",
arduino: '',
armasm: '',
avrasm: '',
bash: '',
c: '',
clojure: '',
coffeescript: '',
cpp: '',
csharp: '󰌛',
css: '',
dockerfile: '󰡨',
go: '',
gradle: '',
haskell: '',
html: '',
java: '',
javascript: '󰌞',
json: '',
latex: '',
lua: '󰢱',
makefile: '',
markdown: '',
mipsasm: '',
nginx: '',
nix: '󱄅',
php: '',
prolog: '',
python: '',
r: '󰟔',
ruby: '',
rust: '',
scss: '',
shell: '',
typescript: '󰛦',
wasm: '',
x86asm: '',
xml: '󰗀',
);
@each $lang, $content in $languages-map {
[data-language="#{$lang}"]:before {
content: $content;
font-size: 1.1rem;
color: $primary_fg;
padding-right: 0.5rem;
}
[data-language='#{$lang}']:before {
content: $content;
font-size: 1.1rem;
color: $primary_fg;
padding-right: 0.5rem;
}
}
pre {
padding: 5px;
overflow-x: scroll;
padding: 5px;
overflow-x: scroll;
code.hljs {
color: $default_fg;
background: transparent;
}
code.hljs {
color: $default_fg;
background: transparent;
}
}
code {
& .hljs-keyword {
color: $mauve;
}
& .hljs-keyword {
color: $mauve;
}
& .hljs-built_in {
color: $red;
}
& .hljs-built_in {
color: $red;
}
& .hljs-type {
color: $yellow;
}
& .hljs-type {
color: $yellow;
}
& .hljs-literal,
& .hljs-number {
color: $orange;
}
& .hljs-literal,
& .hljs-number {
color: $orange;
}
& .hljs-operator {
color: $teal;
}
& .hljs-operator {
color: $teal;
}
& .hljs-punctuation {
color: $lightteal;
}
& .hljs-punctuation {
color: $lightteal;
}
& .hljs-property,
& .hljs-variable.language_,
& .hljs-symbol {
color: $teal;
}
& .hljs-property,
& .hljs-variable.language_,
& .hljs-symbol {
color: $teal;
}
& .hljs-regexp {
color: $pink;
}
& .hljs-regexp {
color: $pink;
}
& .hljs-string,
& .hljs-char.escape_,
& .hljs-subst {
color: $green;
}
& .hljs-string,
& .hljs-char.escape_,
& .hljs-subst {
color: $green;
}
& .hljs-comment {
color: $grey;
}
& .hljs-comment {
color: $grey;
}
& .hljs-doctag {
color: $red;
}
& .hljs-doctag {
color: $red;
}
& .hljs-meta,
& .hljs-title.function_,
& .hljs-section {
color: $orange;
}
& .hljs-meta,
& .hljs-title.function_,
& .hljs-section {
color: $orange;
}
& .hljs-tag,
& .hljs-attribute {
color: $lightgrey;
}
& .hljs-tag,
& .hljs-attribute {
color: $lightgrey;
}
& .hljs-name,
& .hljs-selector-attr {
color: $mauve;
}
& .hljs-name,
& .hljs-selector-attr {
color: $mauve;
}
& .hljs-params,
& .hljs-selector-class,
& .hljs-template-variable {
color: $default_fg;
}
& .hljs-params,
& .hljs-selector-class,
& .hljs-template-variable {
color: $default_fg;
}
& .hljs-selector-tag {
color: $yellow;
}
& .hljs-selector-tag {
color: $yellow;
}
& .hljs-selector-id {
color: $blue;
}
& .hljs-selector-id {
color: $blue;
}
& .hljs-bullet,
& .hljs-code,
& .hljs-formula {
color: $teal;
}
& .hljs-bullet,
& .hljs-code,
& .hljs-formula {
color: $teal;
}
& .hljs-emphasis {
color: $red;
font-style: italic;
}
& .hljs-emphasis {
color: $red;
font-style: italic;
}
& .hljs-strong {
color: $red;
font-weight: bold;
}
& .hljs-strong {
color: $red;
font-weight: bold;
}
& .hljs-link {
color: $lightblue;
font-style: italic;
}
& .hljs-link {
color: $lightblue;
font-style: italic;
}
& .hljs-quote {
color: $green;
font-style: italic;
}
& .hljs-quote {
color: $green;
font-style: italic;
}
& .hljs-addition {
color: $green;
background: rgba(166, 227, 161, 0.15);
}
& .hljs-addition {
color: $green;
background: rgba(166, 227, 161, 0.15);
}
& .hljs-deletion {
color: $red;
background: rgba(243, 139, 168, 0.15);
}
& .hljs-deletion {
color: $red;
background: rgba(243, 139, 168, 0.15);
}
}

View File

@@ -3,6 +3,7 @@ import { resetCss } from '../style';
export const initializeHotReload = async (): Promise<void> => {
const monitorList = [
`${SRC_DIR}/src/scss/main.scss`,
`${SRC_DIR}/src/scss/style/bar`,
`${SRC_DIR}/src/scss/style/common`,
`${SRC_DIR}/src/scss/style/menus`,