13 | Halloy is an open-source IRC client written in Rust, with the Iced GUI library.
14 | It aims to provide a simple and fast client for Mac, Windows, and Linux platforms.
15 |
23 | https://raw.githubusercontent.com/squidowl/halloy/7aec18d29ffb1d3605131667c6e11d8cef6ada7b/assets/screenshot.png
24 |
25 |
26 | The Squidowl Development Team
27 |
28 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/assets/linux/org.squidowl.halloy.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Name=Halloy
3 | Comment=IRC client written in Rust
4 | Type=Application
5 | Keywords=IRC;IM;Chat;
6 | Categories=Network;IRCClient;
7 | Exec=halloy %U
8 | Icon=org.squidowl.halloy
9 | StartupWMClass=org.squidowl.halloy
10 | MimeType=x-scheme-handler/irc;x-scheme-handler/ircs;x-scheme-handler/halloy;
11 | Terminal=false
12 |
--------------------------------------------------------------------------------
/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/assets/logo.png
--------------------------------------------------------------------------------
/assets/macos/Halloy.app/Contents/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
7 | CFBundleExecutable
8 | halloy
9 | CFBundleIdentifier
10 | org.squidowl.halloy
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | Halloy
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | {{ VERSION }}
19 | CFBundleSupportedPlatforms
20 |
21 | MacOSX
22 |
23 | CFBundleVersion
24 | {{ BUILD }}
25 | CFBundleIconFile
26 | halloy.icns
27 | NSHighResolutionCapable
28 |
29 | NSMainNibFile
30 |
31 | NSSupportsAutomaticGraphicsSwitching
32 |
33 | CFBundleDisplayName
34 | Halloy
35 | NSRequiresAquaSystemAppearance
36 | NO
37 | CFBundleURLTypes
38 |
39 |
40 | CFBundleURLName
41 | Halloy
42 | CFBundleURLSchemes
43 |
44 | irc
45 | ircs
46 | halloy
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/assets/macos/Halloy.app/Contents/Resources/halloy.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/assets/macos/Halloy.app/Contents/Resources/halloy.icns
--------------------------------------------------------------------------------
/assets/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/assets/screenshot.png
--------------------------------------------------------------------------------
/assets/themes/ferra.toml:
--------------------------------------------------------------------------------
1 | [general]
2 | background = "#2b292d"
3 | horizontal_rule = "#323034"
4 | unread_indicator = "#ffa07a"
5 | border = "#4f474d"
6 |
7 | [text]
8 | primary = "#fecdb2"
9 | secondary = "#AB8A79"
10 | tertiary = "#d7bde2"
11 | success = "#b1b695"
12 | error = "#e06b75"
13 |
14 | [buffer]
15 | background = "#242226"
16 | background_text_input = "#1D1B1E"
17 | background_title_bar = "#222024"
18 | timestamp = "#685650"
19 | action = "#b1b695"
20 | topic = "#AB8A79"
21 | highlight = "#473f30"
22 | code = "#af8d9f"
23 | nickname = "#f6b6c9"
24 | url = "#d1d1e0"
25 | selection = "#453d41"
26 | border_selected = "#7D6E76"
27 |
28 | [buffer.server_messages]
29 | default = "#f5d76e"
30 |
31 | [buttons.primary]
32 | background = "#2b292d"
33 | background_hover = "#242226"
34 | background_selected = "#1d1b1e"
35 | background_selected_hover = "#0D0C0D"
36 |
37 | [buttons.secondary]
38 | background = "#323034"
39 | background_hover = "#3e3c41"
40 | background_selected = "#606155"
41 | background_selected_hover = "#6F7160"
42 |
--------------------------------------------------------------------------------
/assets/windows/halloy.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/assets/windows/halloy.ico
--------------------------------------------------------------------------------
/assets/windows/halloy.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PerMonitorV2, unaware
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/assets/windows/halloy.rc:
--------------------------------------------------------------------------------
1 | #define IDI_ICON 0x101
2 |
3 | IDI_ICON ICON "halloy.ico"
4 |
5 | #define RT_MANIFEST 24
6 |
7 | 1 RT_MANIFEST "halloy.manifest"
8 |
--------------------------------------------------------------------------------
/book/.gitignore:
--------------------------------------------------------------------------------
1 | book
2 |
--------------------------------------------------------------------------------
/book/CNAME:
--------------------------------------------------------------------------------
1 | halloy.chat
2 |
--------------------------------------------------------------------------------
/book/book.toml:
--------------------------------------------------------------------------------
1 | [book]
2 | authors = ["Casper Rogild Storm", "Cory Forsstrom"]
3 | language = "en"
4 | multilingual = false
5 | src = "src"
6 |
7 | [output.html]
8 | cname = "halloy.chat"
9 | git-repository-url = "https://github.com/squidowl/halloy"
10 | edit-url-template = "https://github.com/squidowl/halloy/edit/main/book/{path}"
11 |
--------------------------------------------------------------------------------
/book/src/README.md:
--------------------------------------------------------------------------------
1 | # Halloy
2 |
3 |
4 |
5 | 
6 |
7 | **Halloy** is an open-source IRC client written in Rust, with the [iced](https://github.com/iced-rs/iced/) GUI library. It aims to provide a simple and fast client for Mac, Windows, and Linux platforms.
8 |
9 | * IRCv3.2 capabilities
10 | * [account-notify](https://ircv3.net/specs/extensions/account-notify)
11 | * [away-notify](https://ircv3.net/specs/extensions/away-notify)
12 | * [batch](https://ircv3.net/specs/extensions/batch)
13 | * [cap-notify](https://ircv3.net/specs/extensions/capability-negotiation.html#cap-notify)
14 | * [chathistory](https://ircv3.net/specs/extensions/chathistory)
15 | * [chghost](https://ircv3.net/specs/extensions/chghost)
16 | * [echo-message](https://ircv3.net/specs/extensions/echo-message)
17 | * [extended-join](https://ircv3.net/specs/extensions/extended-join)
18 | * [invite-notify](https://ircv3.net/specs/extensions/invite-notify)
19 | * [labeled-response](https://ircv3.net/specs/extensions/labeled-response)
20 | * [message-tags](https://ircv3.net/specs/extensions/message-tags)
21 | * [Monitor](https://ircv3.net/specs/extensions/monitor)
22 | * [msgid](https://ircv3.net/specs/extensions/message-ids)
23 | * [multi-prefix](https://ircv3.net/specs/extensions/multi-prefix)
24 | * [read-marker](https://ircv3.net/specs/extensions/read-marker)
25 | * [sasl-3.1](https://ircv3.net/specs/extensions/sasl-3.1)
26 | * [server-time](https://ircv3.net/specs/extensions/server-time)
27 | * [setname](https://ircv3.net/specs/extensions/setname.html)
28 | * [Standard Replies](https://ircv3.net/specs/extensions/standard-replies)
29 | * [userhost-in-names](https://ircv3.net/specs/extensions/userhost-in-names)
30 | * [`UTF8ONLY`](https://ircv3.net/specs/extensions/utf8-only)
31 | * [`WHOX`](https://ircv3.net/specs/extensions/whox)
32 | * SASL support
33 | * DCC Send
34 | * Keyboard shortcuts
35 | * Auto-completion for nicknames, commands, and channels
36 | * Notifications support
37 | * Multiple channels at the same time across servers
38 | * Command bar for for quick actions
39 | * [Custom themes](https://themes.halloy.chat)
40 | * Portable mode
41 |
42 | ## Contributing
43 | Halloy is free and open source. You can find the source code as well as report issues and feature requests on [GitHub](https://github.com/squidowl/halloy).
44 |
--------------------------------------------------------------------------------
/book/src/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | [Halloy](README.md)
4 |
5 | - [Installation](installation.md)
6 | - [Getting started](guides/getting-started.md)
7 | - [Get in touch](get-in-touch.md)
8 |
9 | # Guides
10 |
11 | - [Connect with soju](guides/connect-with-soju.md)
12 | - [Connect with ZNC](guides/connect-with-znc.md)
13 | - [Portable mode](guides/portable-mode.md)
14 | - [Multiple servers](guides/multiple-servers.md)
15 | - [Storing passwords in a File](guides/password-file.md)
16 | - [Text Formatting](guides/text-formatting.md)
17 | - [Monitor users](guides/monitor-users.md)
18 | - [YAML migration](guides/migrating-from-yaml.md)
19 |
20 | # Configuration
21 |
22 | - [Configuration](configuration/README.md)
23 | - [Actions](configuration/actions.md)
24 | - [Buffer](configuration/buffer.md)
25 | - [CTCP](configuration/ctcp.md)
26 | - [File Transfer](configuration/file_transfer.md)
27 | - [Font](configuration/font.md)
28 | - [Highlights](configuration/highlights.md)
29 | - [Keyboard](configuration/keyboard.md)
30 | - [Notifications](configuration/notifications.md)
31 | - [Pane](configuration/pane.md)
32 | - [Proxy](configuration/proxy.md)
33 | - [Preview](configuration/preview.md)
34 | - [Scale factor](configuration/scale-factor.md)
35 | - [Servers](configuration/servers.md)
36 | - [Sidebar](configuration/sidebar.md)
37 | - [Themes](configuration/themes/README.md)
38 | - [Community](configuration/themes/community.md)
39 | - [Base16](configuration/themes/base16.md)
40 | - [Tooltips](configuration/tooltips.md)
41 | - [URL Schemes](url-schemes.md)
42 | - [Commands](commands.md)
43 |
--------------------------------------------------------------------------------
/book/src/commands.md:
--------------------------------------------------------------------------------
1 | # Commands
2 |
3 | Commands in Halloy are prefixed with `/`.
4 |
5 | Example
6 |
7 | ```
8 | /me says halloy!
9 | ```
10 |
11 | Halloy will first try to run below commands, and lastly send it directly to the server.
12 |
13 | | Command | Alias | Description |
14 | | --------- | ---------- | ------------------------------------------------------------- |
15 | | `away` | | Mark yourself as away. If already away, the status is removed |
16 | | `join` | `j` | Join channel(s) with optional key(s) |
17 | | `me` | `describe` | Send an action message to the channel |
18 | | `mode` | `m` | Set mode(s) on a channel or retrieve the current mode(s) set |
19 | | `monitor` | | System to notify when users become online/offline |
20 | | `msg` | `query` | Open a query with a nickname and send an optional message |
21 | | `nick` | | Change your nickname on the current server |
22 | | `part` | `leave` | Leave channel(s) with an optional reason |
23 | | `quit` | | Disconnect from the server with an optional reason |
24 | | `raw` | | Send data to the server without modifying it |
25 | | `topic` | `t` | Retrieve the topic of a channel or set a new topic |
26 | | `whois` | | Retrieve information about user(s) |
27 | | `ctcp` | | Client-To-Client requests |
28 |
--------------------------------------------------------------------------------
/book/src/configuration/README.md:
--------------------------------------------------------------------------------
1 | # Configuration
2 |
3 | To edit configuration parameters, create a `config.toml` file located in your configuration directory:
4 |
5 | * Windows: `%AppData%\halloy`
6 | * Mac: `~/Library/Application Support/halloy` or `$HOME/.config/halloy`
7 | * Linux: `$XDG_CONFIG_HOME/halloy` or `$HOME/.config/halloy`
8 |
9 | > 💡 You can easily open the config file directory from command bar in Halloy
10 |
11 | The specification for the configuration file format ([TOML](https://toml.io/)) can be found at [https://toml.io/](https://toml.io/).
12 |
13 | Example config for connecting to [Libera](https://libera.chat/):
14 |
15 | ```toml
16 | [servers.liberachat]
17 | nickname = "halloy-user"
18 | server = "irc.libera.chat"
19 | channels = ["#halloy"]
20 |
21 | [buffer.channel.topic]
22 | enabled = true
23 | ```
24 |
--------------------------------------------------------------------------------
/book/src/configuration/ctcp.md:
--------------------------------------------------------------------------------
1 | # `[ctcp]`
2 |
3 | [Client-to-Client Protocol](https://modern.ircdocs.horse/ctcp) response settings.
4 |
5 | **Example**
6 |
7 | ```toml
8 | # Disable responses for TIME and VERSION responses
9 |
10 | [ctcp]
11 | time = false
12 | version = false
13 | ```
14 |
15 | # `ping`
16 |
17 | Whether Halloy will respond to a [CTCP PING](https://modern.ircdocs.horse/ctcp#ping) message.
18 |
19 | ```toml
20 | # Type: boolean
21 | # Values: true, false
22 | # Default: true
23 |
24 | [ctcp]
25 | ping = true
26 | ```
27 |
28 | # `source`
29 |
30 | Whether Halloy will respond to a [CTCP TIME](https://modern.ircdocs.horse/ctcp#source) message.
31 |
32 | ```toml
33 | # Type: boolean
34 | # Values: true, false
35 | # Default: true
36 |
37 | [ctcp]
38 | source = true
39 | ```
40 |
41 | # `time`
42 |
43 | Whether Halloy will respond to a [CTCP TIME](https://modern.ircdocs.horse/ctcp#time) message.
44 |
45 | ```toml
46 | # Type: boolean
47 | # Values: true, false
48 | # Default: true
49 |
50 | [ctcp]
51 | time = true
52 | ```
53 |
54 | # `version`
55 |
56 | Whether Halloy will respond to a [CTCP VERSION](https://modern.ircdocs.horse/ctcp#version) message.
57 |
58 | ```toml
59 | # Type: boolean
60 | # Values: true, false
61 | # Default: true
62 |
63 | [ctcp]
64 | version = true
65 | ```
66 |
--------------------------------------------------------------------------------
/book/src/configuration/file_transfer.md:
--------------------------------------------------------------------------------
1 | # `[file_transfer]`
2 |
3 | File transfer configuration options.
4 |
5 | ## `save_directory`
6 |
7 | Default directory to save files in. If not set, user will see a file dialog.
8 |
9 | ```toml
10 | # Type: string
11 | # Values: any string
12 | # Default: not set
13 |
14 | [file_transfer]
15 | save_directory = "/Users/halloy/Downloads"
16 | ```
17 |
18 | ## `passive`
19 |
20 | If true, act as the "client" for the transfer. Requires the remote user act as the [server](#file_transferserver).
21 |
22 | ```toml
23 | # Type: boolean
24 | # Values: true, false
25 | # Default: true
26 |
27 | [file_transfer]
28 | passive = true
29 | ```
30 |
31 | ## `timeout`
32 |
33 | Time (in seconds) to wait before timing out a transfer waiting to be accepted.
34 |
35 | ```toml
36 | # Type: integer
37 | # Values: any positive integer
38 | # Default: 300
39 |
40 | [file_transfer]
41 | timeout = 300
42 | ```
43 |
44 | # `[file_transfer.server]`
45 |
46 | This section is **required** if `passive = false`. One side of the file transfer must
47 | operate as the "server", who the other user connects with to establish a connection.
48 |
49 | ## `public_address`
50 |
51 | Address advertised to the remote user to connect to.
52 |
53 | ```toml
54 | # Type: string
55 | # Values: any string
56 | # Default: not set
57 |
58 | [file_transfer.server]
59 | public_address = ""
60 | ```
61 |
62 | ## `bind_address`
63 |
64 | Address to bind to when accepting connections.
65 |
66 | ```toml
67 | # Type: string
68 | # Values: any string
69 | # Default: not set
70 |
71 | [file_transfer.server]
72 | bind_address = ""
73 | ```
74 |
75 | ## `bind_port_first`
76 |
77 | First port in port range to bind to.
78 |
79 | ```toml
80 | # Type: integer
81 | # Values: any positive integer
82 | # Default: not set
83 |
84 | [file_transfer.server]
85 | bind_port_first = "1024"
86 | ```
87 |
88 | ## `bind_port_last`
89 |
90 | Last port in port range to bind to.
91 |
92 | ```toml
93 | # Type: integer
94 | # Values: any positive integer
95 | # Default: not set
96 |
97 | [file_transfer.server]
98 | bind_port_last = "5000"
99 | ```
100 |
--------------------------------------------------------------------------------
/book/src/configuration/font.md:
--------------------------------------------------------------------------------
1 | # `[font]`
2 |
3 | Application wide font settings.
4 |
5 | > ⚠️ Changes to font settings require an application restart to take effect.
6 |
7 | > 💡 If Halloy is unable to load the specified font & weight, an fallback font may be used. If the font looks wrong, double-check the family name and that the font family has the specified weight.
8 |
9 | ## `family`
10 |
11 | Monospaced font family to use.
12 |
13 | ```toml
14 | # Type: string
15 | # Values: any string
16 | # Default: not set
17 | #
18 | # Note: Iosevka Term is provided by the application, and used by default.
19 |
20 | [font]
21 | family = "Comic Mono"
22 | ```
23 |
24 | ## `size`
25 |
26 | Font size.
27 |
28 | ```toml
29 | # Type: integer
30 | # Values: any positive integer
31 | # Default: 13
32 |
33 | [font]
34 | size = 13
35 | ```
36 |
37 | ## `size`
38 |
39 | Font weight.
40 |
41 | ```toml
42 | # Type: string
43 | # Values: "thin", "extra-light", "light", "normal", "medium", "semibold", "bold", "extra-bold", and "black"
44 | # Default: "normal"
45 |
46 | [font]
47 | weight = "light"
48 | ```
49 |
50 | ## `size`
51 |
52 | Bold font weight. If not set, then the font weight three steps above the regular font weight (e.g. font weight `"light"` → bold font weight `"semibold"`).
53 |
54 | ```toml
55 | # Type: string
56 | # Values: "thin", "extra-light", "light", "normal", "medium", "semibold", "bold", "extra-bold", and "black"
57 | # Default: not set
58 |
59 | [font]
60 | bold-weight = "semibold"
61 | ```
62 |
--------------------------------------------------------------------------------
/book/src/configuration/highlights.md:
--------------------------------------------------------------------------------
1 | # `[highlights]`
2 |
3 | Application wide highlights.
4 |
5 | **Example**
6 |
7 | ```toml
8 | # Enable nickname highlights only in channel #halloy.
9 | [highlights.nickname]
10 | exclude = ["*"]
11 | include = ["#halloy"]
12 |
13 | # Highlight on 'boat' and 'car' in any channel.
14 | [[highlights.match]]
15 | words = ["boat", "car"]
16 | case_insensitive = true
17 |
18 | # Highlight when regex matches in any channel except #noisy-channel.
19 | [[highlights.match]]
20 | regex = '''(?i)\bcasper\b'''
21 | exclude = ["#noisy-channel"]
22 | ```
23 |
24 | ## `[highlights.nickname]`
25 |
26 | Nickname highlights.
27 |
28 | ### `exclude`
29 |
30 | Channels in which you won’t be highlighted.
31 | If you pass `["#halloy"]`, you won’t be highlighted in that channel. You can also exclude all channels by using a wildcard: `["*"]`.
32 |
33 | ```toml
34 | # Type: array of strings
35 | # Values: array of any strings
36 | # Default: []
37 |
38 | [highlights.nickname]
39 | exclude = ["*"]
40 | ```
41 |
42 | ### `include`
43 |
44 | Channels in which you will be highlighted, only useful when combined with `exclude = ["*"]`.
45 | If you pass `["#halloy"]`, you will only be highlighted in that channel.
46 |
47 | ```toml
48 | # Type: array of strings
49 | # Values: array of any strings
50 | # Default: ["*"]
51 |
52 | [highlights.nickname]
53 | exclude = ["*"]
54 | include = ["#halloy"]
55 | ```
56 |
57 | ## `[[highlights.match]]`
58 |
59 | Highlight based on matches.
60 |
61 | ### `words`
62 |
63 | You can set words to be highlighted when they are written.
64 |
65 | Example shows word matches, which will trigger on `"word1"`, `"word2"` or `"word3"` in any channel.
66 |
67 | ```toml
68 | # Type: array of strings
69 | # Values: array of any strings
70 | # Default: []
71 |
72 | [[highlights.match]]
73 | words = ["word1", "word2", "word3"]
74 | ```
75 |
76 | ### `case_insensitive`
77 |
78 | This option is only available when using `words` as the match type.
79 | You can choose whether or not to trigger regardless of case.
80 |
81 | ```toml
82 | # Type: boolean
83 | # Values: true, false
84 | # Default: false
85 |
86 | [[highlights.match]]
87 | words = ["word1", "word2", "word3"]
88 | case_insensitive = true
89 | ```
90 |
91 | ### `regex`
92 |
93 | Match based on regex.
94 |
95 |
96 |
97 | Use toml multi-line literal strings `'''\bfoo'd\b'''` when writing a regex. This allows you to write write the regex without
98 | escaping. You can also use a literal string `'\bfoo\b'`, but then you can't use `'` inside the string.
99 |
100 | Without literal strings, you'd have to write the above as `"\\bfoo'd\\b"`
101 |
102 |
103 |
104 | Example shows a regex that matches the word "casper", regardless of case and only when it appears as a whole word in any channel.
105 |
106 | ```toml
107 | # Type: string
108 | # Values: any string
109 | # Default: not set
110 |
111 | [[highlights.match]]
112 | regex = '''(?i)\bcasper\b'''
113 | ```
114 |
115 | ### `exclude`
116 |
117 | Channels in which you won’t be highlighted.
118 | If you pass `["#halloy"]`, you won’t be highlighted in that channel. You can also exclude all channels by using a wildcard: `["*"]`.
119 |
120 | Example shows a regex match which will be excluded in from `#noisy-channel`
121 |
122 | ```toml
123 | # Type: array of strings
124 | # Values: array of any strings
125 | # Default: []
126 |
127 | [[highlights.match]]
128 | regex = '''(?i)\bcasper\b'''
129 | exclude = ["#noisy-channel"]
130 | ```
131 |
132 | ### `include`
133 |
134 | Channels in which you will be highlighted, only useful when combined with `exclude = ["*"]`.
135 | If you pass `["#halloy"]`, you will only be highlighted in that channel.
136 |
137 | Example shows a words match which will only try to match in `#halloy` channel.
138 |
139 | ```toml
140 | # Type: array of strings
141 | # Values: array of any strings
142 | # Default: ["*"]
143 |
144 | [[highlights.match]]
145 | words = ["word1", "word2", "word3"]
146 | exclude = ["*"]
147 | include = ["#halloy"]
148 | ```
149 |
--------------------------------------------------------------------------------
/book/src/configuration/notifications.md:
--------------------------------------------------------------------------------
1 | # `[notifications]`
2 |
3 | Customize and enable notifications.
4 |
5 | **Example**
6 |
7 | ```toml
8 | [notifications]
9 | direct_message = { sound = "peck", show_toast = true }
10 |
11 | [notifications.highlight]
12 | sound = "dong"
13 | exclude = ["NickServ", "#halloy"]
14 | ```
15 |
16 | Following notifications are available:
17 |
18 | | Name | Description |
19 | | ----------------------- | -------------------------------------------------- |
20 | | `connected` | Triggered when a server is connected |
21 | | `direct_message` | Triggered when a direct message is received |
22 | | `disconnected` | Triggered when a server disconnects |
23 | | `file_transfer_request` | Triggered when a file transfer request is received |
24 | | `highlight` | Triggered when you were highlighted in a buffer |
25 | | `monitored_online` | Triggered when a user you're monitoring is online |
26 | | `monitored_offline` | Triggered when a user you're monitoring is offline |
27 | | `reconnected` | Triggered when a server reconnects |
28 |
29 |
30 | ## `sound`
31 |
32 | Notification sound.
33 | Supports both built-in sounds, and external sound files (`mp3`, `ogg`, `flac` or `wav` placed inside the `sounds` folder within the configuration directory).
34 |
35 | ```toml
36 | # Type: string
37 | # Values: "dong", "peck", "ring", "squeak", "whistle", "bonk", "sing" or external sound.
38 | # Default: not set
39 |
40 | [notifications.]
41 | sound = "dong"
42 | ```
43 |
44 | ## `show_toast`
45 |
46 | Notification should trigger a OS toast.
47 |
48 | ```toml
49 | # Type: boolean
50 | # Values: true, false
51 | # Default: false
52 |
53 | [notifications.]
54 | show_toast = true
55 | ```
56 |
57 | ## `delay`
58 |
59 | Delay in milliseconds before triggering the next notification.
60 |
61 | ```toml
62 | # Type: integer
63 | # Values: any positive integer
64 | # Default: 500
65 |
66 | [notifications.]
67 | delay = 250
68 | ```
69 |
70 | ## `exclude`
71 |
72 | Exclude notifications for nicks (and/or channels in `highlight`'s case).
73 |
74 | Only available for `direct_message`, `highlight` and `file_transfer_request`
75 | notifications.
76 |
77 | You can also exclude all nicks/channels by using a wildcard: `["*"]` or `["all"]`.
78 |
79 | ```toml
80 | # Type: array of strings
81 | # Values: array of strings
82 | # Default: []
83 |
84 | [notifications.]
85 | exclude = ["HalloyUser1"]
86 |
87 | [notifications.highlight]
88 | exclude = ["HalloyUser1", "#halloy"]
89 | ```
90 |
91 | ## `include`
92 |
93 | Include notifications for nicks (and/or channels in `highlight`'s case).
94 |
95 | Only available for `direct_message`, `highlight` and `file_transfer_request`
96 | notifications.
97 |
98 | The include rule takes priority over exclude, so you can use both together.
99 | For example, you can exclude all nicks with `["*"]` for `direct_message` and
100 | then only include a few specific nicks to receive `direct_message` notifications
101 | from.
102 |
103 | ```toml
104 | # Type: array of strings
105 | # Values: array of strings
106 | # Default: []
107 |
108 | [notifications.]
109 | include = ["HalloyUser1"]
110 |
111 | [notifications.highlight]
112 | include = ["HalloyUser1", "#halloy"]
113 | ```
114 |
--------------------------------------------------------------------------------
/book/src/configuration/pane.md:
--------------------------------------------------------------------------------
1 | # `[pane]`
2 |
3 | Pane settings for Halloy. A pane contains a [buffer](../configuration//buffer.md).
4 |
5 | ## `split_axis`
6 |
7 | Default axis used when splitting a pane (i.e. default orientation of the divider between panes).
8 |
9 | ```toml
10 | # Type: string
11 | # Values: "horizontal", "vertical"
12 | # Default: "horizontal"
13 |
14 | [pane]
15 | split_axis = "vertical"
16 | ```
17 |
--------------------------------------------------------------------------------
/book/src/configuration/proxy.md:
--------------------------------------------------------------------------------
1 | # `[proxy]`
2 |
3 | Proxy settings for Halloy.
4 |
5 | 1. [http](#proxyhttp)
6 | 2. [socks5](#proxysocks5)
7 | 3. [tor](#proxytor)
8 |
9 | ## `[proxy.http]`
10 |
11 | Http proxy settings.
12 |
13 | ### `host`
14 |
15 | Proxy host to connect to.
16 |
17 | ```toml
18 | # Type: string
19 | # Values: any string
20 | # Default: not set
21 |
22 | # Required
23 |
24 | [proxy.http]
25 | host = "192.168.1.100"
26 | ```
27 |
28 | ### `port`
29 |
30 | Proxy port to connect on.
31 |
32 | ```toml
33 | # Type: integer
34 | # Values: any positive integer
35 | # Default: not set
36 |
37 | # Required
38 |
39 | [proxy.http]
40 | port = 1080
41 | ```
42 |
43 | ### `username`
44 |
45 | Proxy username.
46 |
47 | ```toml
48 | # Type: string
49 | # Values: any string
50 | # Default: not set
51 |
52 | # Optional
53 |
54 | [proxy.http]
55 | username = "username"
56 | ```
57 |
58 | ### `password`
59 |
60 | Proxy password.
61 |
62 | ```toml
63 | # Type: string
64 | # Values: any string
65 | # Default: not set
66 |
67 | # Optional
68 |
69 | [proxy.http]
70 | password = "password"
71 | ```
72 |
73 | ## Example
74 |
75 | ```toml
76 | [proxy.http]
77 | host = "192.168.1.100"
78 | port = 1080
79 | username = "username"
80 | password = "password"
81 | ```
82 |
83 | ## `[proxy.socks5]`
84 |
85 | Socks5 proxy settings.
86 |
87 | ### `host`
88 |
89 | Proxy host to connect to.
90 |
91 | ```toml
92 | # Type: string
93 | # Values: any string
94 | # Default: not set
95 |
96 | # Required
97 |
98 | [proxy.socks5]
99 | host = "192.168.1.100"
100 | ```
101 |
102 | ### `port`
103 |
104 | Proxy port to connect on.
105 |
106 | ```toml
107 | # Type: integer
108 | # Values: any positive integer
109 | # Default: not set
110 |
111 | # Required
112 |
113 | [proxy.socks5]
114 | port = 1080
115 | ```
116 |
117 | ### `username`
118 |
119 | Proxy username.
120 |
121 | ```toml
122 | # Type: string
123 | # Values: any string
124 | # Default: not set
125 |
126 | # Optional
127 |
128 | [proxy.socks5]
129 | username = "username"
130 | ```
131 |
132 | ### `password`
133 |
134 | Proxy password.
135 |
136 | ```toml
137 | # Type: string
138 | # Values: any string
139 | # Default: not set
140 |
141 | # Optional
142 |
143 | [proxy.socks5]
144 | password = "password"
145 | ```
146 |
147 | ## Example
148 |
149 | ```toml
150 | [proxy.socks5]
151 | host = "192.168.1.100"
152 | port = 1080
153 | username = "username"
154 | password = "password"
155 | ```
156 |
157 | ## `[proxy.tor]`
158 |
159 | Tor proxy settings. Utilizes the [arti](https://arti.torproject.org/) to integrate Tor natively.
160 | It accepts no further configuration.
161 |
162 | ## Example
163 |
164 | ```toml
165 | [proxy.tor]
166 | ```
167 |
--------------------------------------------------------------------------------
/book/src/configuration/scale-factor.md:
--------------------------------------------------------------------------------
1 | # `[scale_factor]`
2 |
3 | Application wide scale factor.
4 | Note: `scale_factor` is a root key, so it must be placed before any section.
5 |
6 | ```toml
7 | # Type: float
8 | # Values: 0.1 .. 3.0
9 | # Default: 1.0
10 |
11 | scale_factor = 1.0
12 | ```
13 |
--------------------------------------------------------------------------------
/book/src/configuration/sidebar.md:
--------------------------------------------------------------------------------
1 | # `[sidebar]`
2 |
3 | Sidebar settings for Halloy.
4 |
5 | ## `unread_indicator`
6 |
7 | Unread buffer indicator style.
8 |
9 | ```toml
10 | # Type: string
11 | # Values: "dot", "title", "none"
12 | # Default: "dot"
13 |
14 | [sidebar]
15 | unread_indicator = "dot"
16 | ```
17 |
18 | ## `position`
19 |
20 | Sidebar position within the application window.
21 |
22 | ```toml
23 | # Type: string
24 | # Values: "left", "top", "right", "bottom"
25 | # Default: "left"
26 |
27 | [sidebar]
28 | position = "left"
29 | ```
30 |
31 | ## `max_width`
32 |
33 | Specify sidebar max width in pixels. Only used if `position` is `"left"` or `"right"`.
34 |
35 | ```toml
36 | # Type: integer
37 | # Values: any positive integer
38 | # Default: not set
39 |
40 | [sidebar]
41 | max_width = 200
42 | ```
43 |
44 | ## `show_menu_button`
45 |
46 | Show or hide the user menu button in the sidemenu.
47 |
48 | ```toml
49 | # Type: bool
50 | # Values: true, false
51 | # Default: true
52 |
53 | [sidebar]
54 | show_menu_button = true
55 | ```
56 |
--------------------------------------------------------------------------------
/book/src/configuration/themes/README.md:
--------------------------------------------------------------------------------
1 | # Themes
2 |
3 | ## Example
4 |
5 | ```toml
6 | # Static
7 | theme = "ferra"
8 |
9 | # Dynamic
10 | theme = { light = "ferra-light", dark = "ferra" }
11 | ```
12 |
13 | Note: `theme` is a root key, so it must be placed before any section.
14 |
15 | ## `theme`
16 |
17 | Specify the theme name(s) to use. The theme must correspond to a file located in the `themes` folder, which can be found in the Halloy configuration directory. The default theme in Halloy is [Ferra](https://github.com/casperstorm/ferra/).
18 |
19 | When a dynamic theme is used, Halloy will match the appearance of the OS.
20 |
21 | - **type**: string or object
22 | - **values**: `""`, `{ light = "", dark = "" }`
23 | - **default**: `"ferra"`
24 |
25 | > 💡 See all community created themes [here](./community.md) and base16 themes [here](./base16.md).
26 |
27 | ## Custom themes
28 |
29 | To create a custom theme for Halloy, simply place a theme file (with a `.toml` extension) inside the `themes` folder within the configuration directory.
30 |
31 | ```toml
32 | # Consider we have a theme called "foobar.toml" inside the themes folder.
33 | # Theme is a root key, so it has to be placed before any sections in your config file.
34 |
35 | theme = "foobar"
36 | # .. rest of the configuration file.
37 | ```
38 |
39 | > 💡 Halloy has a built in theme editor which makes theme creation easier
40 |
41 | Each `""` is expected to be a valid hex color. If invalid, or if the key is removed, the color will fallback to transparent. A custom theme is structured as follows:
42 |
43 | ```toml
44 | [general]
45 | background = ""
46 | border = ""
47 | horizontal_rule = ""
48 | unread_indicator = ""
49 |
50 | [text]
51 | primary = ""
52 | secondary = ""
53 | tertiary = ""
54 | success = ""
55 | error = ""
56 |
57 | [buttons.primary]
58 | background = ""
59 | background_hover = ""
60 | background_selected = ""
61 | background_selected_hover = ""
62 |
63 | [buttons.secondary]
64 | background = ""
65 | background_hover = ""
66 | background_selected = ""
67 | background_selected_hover = ""
68 |
69 | [buffer]
70 | action = ""
71 | background = ""
72 | background_text_input = ""
73 | background_title_bar = ""
74 | border = ""
75 | border_selected = ""
76 | code = ""
77 | highlight = ""
78 | nickname = ""
79 | selection = ""
80 | timestamp = ""
81 | topic = ""
82 | url = ""
83 |
84 | [buffer.server_messages]
85 | # Set below if you want to have a unique color for each.
86 | # Otherwise simply set `default` to use that for all server messages.
87 | #
88 | # change_host = ""
89 | # join = ""
90 | # part = ""
91 | # quit = ""
92 | # reply_topic = ""
93 | # monitored_online = ""
94 | # monitored_offline = ""
95 | # standard_reply_fail = ""
96 | # standard_reply_warn = ""
97 | # standard_reply_note = ""
98 | # wallops = ""
99 | default = ""
100 | ```
101 | > 💡 The default Ferra theme toml file can be viewed [here](https://github.com/squidowl/halloy/blob/main/assets/themes/ferra.toml).
102 |
--------------------------------------------------------------------------------
/book/src/configuration/themes/base16.md:
--------------------------------------------------------------------------------
1 | # Base16
2 |
3 | The [base16](https://github.com/chriskempson/base16) color scheme framework
4 | includes hundreds of color schemes build using 16 colors. These color schemes have
5 | are compiled for Halloy in the
6 | [`4e554c4c/base16-halloy`](https://github.com/4e554c4c/base16-halloy)
7 | repository.
8 |
9 | To use these themes, download `themes.tar.gz` from the
10 | [latest release](https://github.com/4e554c4c/base16-halloy/releases/latest)
11 | and unpack it to the `themes` folder in the Halloy configuration directory. Then
12 | you can enable themes individually in `config.toml`.
13 |
14 | **Example**
15 |
16 | ```toml
17 | # Static
18 | theme = "base16-gruvbox-dark-hard"
19 | ```
20 |
--------------------------------------------------------------------------------
/book/src/configuration/themes/community.md:
--------------------------------------------------------------------------------
1 | # Community
2 |
3 | Discover community created themes for Halloy at https://themes.halloy.chat.
4 |
--------------------------------------------------------------------------------
/book/src/configuration/tooltips.md:
--------------------------------------------------------------------------------
1 | # `[tooltips]`
2 |
3 | Control if tooltips are displayed or not.
4 | Note: `tooltips` is a root key, so it must be placed before any section.
5 |
6 | ```toml
7 | # Type: boolean
8 | # Values: true, false
9 | # Default: true
10 |
11 | tooltips = true
12 | ```
13 |
--------------------------------------------------------------------------------
/book/src/get-in-touch.md:
--------------------------------------------------------------------------------
1 | # Get in touch
2 |
3 | Join `#halloy` on `libera.chat` ([link](ircs://irc.libera.chat/#halloy)) if you have questions, looking for help or just want to say hello.
4 | For feature requests or reporting issues, please open a ticket on [GitHub](https://github.com/squidowl/halloy).
5 |
6 | ## Maintainers
7 |
8 | * andymandias ([https://github.com/andymandias](https://github.com/andymandias))
9 | * casperstorm ([https://github.com/casperstorm](https://github.com/casperstorm))
10 | * tarkah ([https://github.com/tarkah](https://github.com/tarkah))
11 |
12 | ## Contributors
13 |
14 | Special thanks to all the people who makes Halloy happens
15 |
16 | * 4e554c4c ([https://github.com/4e554c4c](https://github.com/4e554c4c))
17 | * a-kenji ([https://github.com/a-kenji](https://github.com/a-kenji))
18 | * adamperkowski ([https://github.com/adamperkowski](https://github.com/adamperkowski))
19 | * ameknite ([https://github.com/ameknite](https://github.com/ameknite))
20 | * anarsoul ([https://github.com/anarsoul](https://github.com/anarsoul))
21 | * auronandace ([https://github.com/auronandace](https://github.com/auronandace))
22 | * bbb651 ([https://github.com/bbb651](https://github.com/bbb651))
23 | * Daeraxa ([https://github.com/Daeraxa](https://github.com/Daeraxa))
24 | * englut ([https://github.com/englut](https://github.com/englut))
25 | * funkeleinhorn ([https://github.com/funkeleinhorn](https://github.com/funkeleinhorn))
26 | * ikigai-gh ([https://github.com/ikigai-gh](https://github.com/ikigai-gh))
27 | * jhff ([https://github.com/jhff](https://github.com/jhff))
28 | * KaiKorla ([https://github.com/KaiKorla](https://github.com/KaiKorla))
29 | * ljrk0 ([https://github.com/ljrk0](https://github.com/ljrk0))
30 | * lodenrogue ([https://github.com/lodenrogue](https://github.com/lodenrogue))
31 | * mikemykhaylov ([https://github.com/mikemykhaylov](https://github.com/mikemykhaylov))
32 | * neilalexander ([https://github.com/neilalexander](https://github.com/neilalexander))
33 | * oldgalileo ([https://github.com/oldgalileo](https://github.com/oldgalileo))
34 | * petergam ([https://github.com/petergam](https://github.com/petergam))
35 | * ramajd ([https://github.com/ramajd](https://github.com/ramajd))
36 | * robert-groensfeld ([https://github.com/robert-groensfeld](https://github.com/robert-groensfeld))
37 | * seth0xd ([https://github.com/seth0xd](https://github.com/seth0xd))
38 | * spoisseroux ([https://github.com/spoisseroux](https://github.com/spoisseroux))
39 | * Tea23 ([https://github.com/Tea23](https://github.com/Tea23))
40 | * theRAAPster ([https://github.com/theRAAPster](https://github.com/theRAAPster))
41 | * VioletSpace ([https://github.com/VioletSpace](https://github.com/VioletSpace))
42 | * YouFoundAlpha ([https://github.com/YouFoundAlpha](https://github.com/YouFoundAlpha))
43 |
44 | Did we forget you? We're sorry about that! Feel free to add yourself and create a pull request.
45 |
--------------------------------------------------------------------------------
/book/src/guides/connect-with-soju.md:
--------------------------------------------------------------------------------
1 | # Connect with Soju
2 |
3 | To connect with a [**soju**](https://soju.im/) bouncer, the configuration below can be used as a template. Simply change so it fits your credentials.
4 |
5 | *as of 2025.1 Halloy supports chathistory, so the machinename(like @desktop) is no longer needed*
6 |
7 | ```toml
8 | [servers.libera]
9 | nickname = "casperstorm"
10 | username = "/irc.libera.chat"
11 | server = "irc.squidowl.org"
12 | port = 6697
13 | password = ""
14 | chathistory = true
15 | ```
16 |
17 | You can enable infinite scrolling history as well, if you want to be able to load older messages
18 |
19 | ```toml
20 | [buffer.chathistory]
21 | infinite_scroll = true
22 | ```
23 |
--------------------------------------------------------------------------------
/book/src/guides/connect-with-znc.md:
--------------------------------------------------------------------------------
1 | # Connect with ZNC
2 |
3 | To connect with a [**ZNC**](https://wiki.znc.in/ZNC) bouncer, the configuration below can be used as a template. Simply change so it fits your credentials.
4 |
5 | ```toml
6 | [servers.libera]
7 | nickname = "/"
8 | server = "znc.example.com"
9 | password = ""
10 |
11 | # Depending on your ZNC setup you may need to apply these extra settings:
12 |
13 | # Does your znc use a self-signed or expired certificate? See:
14 | # https://halloy.chat/configuration/servers.html#dangerously_accept_invalid_certs
15 |
16 | # Does your znc listen on a different port? See:
17 | # https://halloy.chat/configuration/servers.html#port
18 |
19 | ```
20 |
--------------------------------------------------------------------------------
/book/src/guides/getting-started.md:
--------------------------------------------------------------------------------
1 | # Getting started
2 |
3 | To get started with Halloy, you need to connect to at least one IRC server. The template config file has been set up with the [Libera](https://libera.chat/) server. However, there are many other servers available: [OFTC](https://www.oftc.net/), [Undernet](https://www.undernet.org/), [EFnet](http://www.efnet.org), [QuakeNet](https://www.quakenet.org/) and [many more](https://netsplit.de/networks/). Halloy can connect to multiple servers at the same time.
4 |
5 | Once connected to a server, you can join channels. This can be done automatically from the config file or manually using the join command: `/join #channel`[^1]. To find channels, you can either use the list command: `/list`, or [browse for channels online](https://netsplit.de/channels/).
6 |
7 | > 💡 Configuration in Halloy happens through a `config.toml` file. See [Configuration](../configuration/).
8 |
9 | Here are a few useful IRC commands for a new user[^2]
10 |
11 | | Command | Example | Description |
12 | | ----------------- | ---------------------- | ------------------------------------------ |
13 | | `/join` | `/join #halloy` | Join a new channel |
14 | | `/part` | `/part #halloy` | Part a channel |
15 | | `/nick` | `/nick halloyisgreat` | Change your nickname |
16 | | `/whois nickname` | `/whois halloyisgreat` | Displays information of nickname requested |
17 | | `/list *keyword*` | `/list *linux*` | List channels. Keyword is optional |
18 |
19 |
20 | [^1]: Channel names always start with a `#` symbol and do not contain spaces.
21 | [^2]: Find more commands [here](https://en.wikipedia.org/wiki/List_of_Internet_Relay_Chat_commands).
22 |
--------------------------------------------------------------------------------
/book/src/guides/migrating-from-yaml.md:
--------------------------------------------------------------------------------
1 | # Migrating from YAML
2 |
3 | Halloy switched configuration file format from YAML to TOML ([PR-278](https://github.com/squidowl/halloy/pull/278))
4 | This page will help you migrate your old `config.yaml` to a new `config.toml` file.
5 |
6 | The basic structure of a TOML file consists of key-value pairs, where keys are strings. There are no nested indentations like YAML, which makes it easier to read and write. Consider the following old YAML config with of two servers in Halloy:
7 |
8 | ```yaml
9 | servers:
10 | libera:
11 | nickname: foobar
12 | server: irc.libera.chat
13 | quakenet:
14 | nickname: barbaz
15 | server: underworld2.no.quakenet.org
16 | port: 6667
17 | use_tls: true
18 | ```
19 |
20 | This now looks the following in TOML
21 |
22 | ```toml
23 | [servers.libera]
24 | nickname = "foobar"
25 | server = "irc.libera.chat"
26 |
27 | [servers.quakenet]
28 | nickname = "barbaz"
29 | server = "underworld2.no.quakenet.org"
30 | port = 6667
31 | use_tls = true
32 | ```
33 |
34 | > 💡 You can convert YAML to TOML using a converter tool like [this one](https://transform.tools/yaml-to-toml). Just note that a few keys and values have be renamed during the conversion process.
35 |
36 | To migrate, and ensure everything is working, make sure to read through the [Configuration](../configuration) section of this book. Here, every configuration option is documented using TOML.
37 |
--------------------------------------------------------------------------------
/book/src/guides/monitor-users.md:
--------------------------------------------------------------------------------
1 | # Monitor users
2 |
3 | Halloy has [monitor](https://ircv3.net/specs/extensions/monitor) support if the server has the IRCv3 Monitor extension.
4 |
5 | > 💡 A protocol for notification of when clients become online/offline
6 |
7 | To use the feature you need to add the user(s) you wish to monitor. This can be done in two ways:
8 |
9 | * You can add a list of user directly to the configuration file. [See configuration option.](../configuration/servers.md#monitor)
10 | * You can add users through `/monitor` directly in Halloy.
11 |
12 | Examples with the `/monitor` command:
13 |
14 | ```toml
15 | /monitor + casperstorm # Add user to list being monitored
16 | /monitor - casperstorm # Remove user from list being monitored
17 | /monitor c # Clear the list of users being monitored
18 | /monitor l # Get list of users being monitored
19 | /monitor s # For each user in the list being monitored, get their current status
20 | ```
21 |
--------------------------------------------------------------------------------
/book/src/guides/multiple-servers.md:
--------------------------------------------------------------------------------
1 | # Multiple servers
2 |
3 | Creating multiple `[servers]` sections lets you connect to multiple servers.
4 | All configuration options can be found [here](../configuration/servers.md).
5 |
6 | ```toml
7 | [servers.liberachat]
8 | nickname = "halloy-user"
9 | server = "irc.libera.chat"
10 | channels = ["#halloy"]
11 |
12 | [servers.oftc]
13 | nickname = "halloy-user"
14 | server = "irc.oftc.net"
15 | channels = ["#asahi-dev"]
16 | ```
17 |
--------------------------------------------------------------------------------
/book/src/guides/password-file.md:
--------------------------------------------------------------------------------
1 | # Storing passwords in a File
2 |
3 | If you need to commit your configuration file to a public repository, you can keep your passwords in a separate file for security. Below is an example of using a file for nickname password for NICKSERV.
4 |
5 |
6 | > 💡 Avoid adding extra lines in the password file, as they will be treated as part of the password.
7 |
8 | > 💡 Shell expansions (e.g. `"~/"` → `"/home/user/"`) are not supported in path strings.
9 |
10 | > 💡 Windows path strings should usually be specified as literal strings (e.g. `'C:\Users\Default\'`), otherwise directory separators will need to be escaped (e.g. `"C:\\Users\\Default\\"`).
11 |
12 | ```toml
13 | [servers.liberachat]
14 | nickname = "foobar"
15 | nick_password_file = "/home/user/config/halloy/password"
16 | server = "irc.libera.chat"
17 | channels = ["#halloy"]
18 | ```
19 |
--------------------------------------------------------------------------------
/book/src/guides/portable-mode.md:
--------------------------------------------------------------------------------
1 | # Portable mode
2 |
3 | To enable portable mode for Halloy, simply place the `config.toml` file in the same directory as the running executable.
4 |
5 | ```
6 | .
7 | ├── Halloy.app
8 | └── config.toml
9 | ```
10 |
--------------------------------------------------------------------------------
/book/src/guides/text-formatting.md:
--------------------------------------------------------------------------------
1 | # Text Formatting
2 |
3 | Text can be formatted in Halloy by using the `/format` (or `/f`) command.
4 |
5 | ## Attributes
6 |
7 | Below is a table with the supported text attributes.
8 |
9 | | Action | Markdown | Token |
10 | | --------------------- | ----------------------- | ------------------------- |
11 | | _Italics_ | `_italic text_` | `$iitalic text$i` |
12 | | **Bold** | `__bold text__` | `$bbold text$b` |
13 | | **_Italic and Bold_** | `___italic and bold___` | `$b$iitalic and bold$i$b` |
14 | | ~~Strikethrough~~ | `~~strikethrough~~` | `$sstrikethrough$s` |
15 | | Underline | - | `$uunderline$u` |
16 | | Code | `` `code` `` | `$mcode$m` |
17 | | Spoiler | `\|\|spoiler\|\|` | - |
18 |
19 | Example
20 |
21 | ```json
22 | /format __this is bold__ $iand this is italic$i
23 | ```
24 |
25 | Will render the following:
26 |
27 | > **this is bold** _and this is italic_
28 |
29 | ## Color
30 |
31 | | Action | Token |
32 | | ----------------------------- | ------- |
33 | | Text color (fg) | `$c0` |
34 | | Text and background (fg & bg) | `$c0,1` |
35 | | End color | `$c` |
36 |
37 | The number next to the `$c` token indicates the color. For a comprehensive list of all numbers, see the following [ircdocs.horse documentation](https://modern.ircdocs.horse/formatting#colors-16-98). Below, the first 00 to 15 colors are defined and have been assigned aliases for convenience.
38 |
39 | Colors
40 |
41 | - 00 - white
42 | - 01 - black
43 | - 02 - blue
44 | - 03 - green
45 | - 04 - red
46 | - 05 - brown
47 | - 06 - magenta
48 | - 07 - orange
49 | - 08 - yellow
50 | - 09 - lightgreen
51 | - 10 - cyan
52 | - 11 - lightcyan
53 | - 12 - lightblue
54 | - 13 - pink
55 | - 14 - grey
56 | - 15 - lightgrey
57 |
58 | Example
59 |
60 | ```
61 | /format $cred,lightgreenfoobar$c
62 | /format $c04,09foobar$c
63 | ```
64 |
65 | Will both render the following:
66 |
67 |
68 | foobar
69 |
70 |
71 | ## Configuration
72 |
73 | By default, Halloy will only format text when using the `/format` command. This, however, can be changed with the `auto_format` configuration option:
74 |
75 | ```toml
76 | [buffer.text_input]
77 | auto_format = "disabled" | "markdown" | "all"
78 | ```
79 |
--------------------------------------------------------------------------------
/book/src/images/animation.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/book/src/images/animation.gif
--------------------------------------------------------------------------------
/book/src/images/banner-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/book/src/images/banner-logo.png
--------------------------------------------------------------------------------
/book/src/installation.md:
--------------------------------------------------------------------------------
1 | # Installing Halloy
2 |
3 | - [Pre-built binaries](#pre-built-binaries)
4 | - [Packaging status](#packaging-status)
5 | - [macOS](#macos)
6 | - [Homebrew](#homebrew)
7 | - [MacPorts](#macports)
8 | - [Linux](#linux)
9 | - [Flatpak](#flatpak)
10 | - [Snapcraft](#snapcraft)
11 | - [Windows](#windows)
12 | - [Winget](#winget)
13 | - [Build from source](#build-from-source)
14 |
15 | > 💡 To get the latest nightly version of Halloy, you can [build from source](#build-from-source).
16 |
17 | ## Pre-built binaries
18 |
19 | Download pre-built binaries from [GitHub](https://github.com/squidowl/halloy/releases) page.
20 |
21 | ### Packaging status
22 |
23 |
24 |
25 |
26 |
27 | ### macOS
28 |
29 | The following third party repositories are available for macOS
30 |
31 | #### Homebrew
32 |
33 | ```
34 | brew install --cask halloy
35 | ```
36 |
37 | #### MacPorts
38 |
39 | ```sh
40 | sudo port install halloy
41 | ```
42 |
43 | ### Linux
44 |
45 | The following third party repositories are available for Linux
46 |
47 | #### Flatpak
48 |
49 | [https://flathub.org/apps/org.squidowl.halloy](https://flathub.org/apps/org.squidowl.halloy)
50 |
51 | #### Snapcraft
52 |
53 | [https://snapcraft.io/halloy](https://snapcraft.io/halloy)
54 |
55 | ### Windows
56 |
57 | #### Winget
58 |
59 | ```sh
60 | winget install squidowl.halloy
61 | ```
62 |
63 | ### Build from source
64 |
65 | Clone the Halloy GitHub repository into a directory of your choice and build with cargo.
66 |
67 | Requirements:
68 |
69 | * [Rust toolchain](https://www.rust-lang.org/tools/install)
70 | * [Git version control system](https://git-scm.com/)
71 |
72 | ```sh
73 | # Clone the repository
74 | git clone https://github.com/squidowl/halloy.git
75 |
76 | cd halloy
77 |
78 | # Build and run
79 | cargo build --release
80 | cargo run --release
81 | ```
82 |
--------------------------------------------------------------------------------
/book/src/url-schemes.md:
--------------------------------------------------------------------------------
1 | # URL Schemes
2 |
3 | Halloy is able to recognize different URL schemes.
4 |
5 | ## IRC and IRCS
6 |
7 | The IRC URL scheme is used to create a new connection to a server.
8 | The format is based on the [URI Syntax](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax).
9 |
10 | ## Format
11 |
12 | ```url
13 | ://:/[#channel[,#channel]]
14 | ```
15 |
16 | | Key | Description |
17 | | --------- | -------------------------------------------------------------- |
18 | | `scheme` | Can be `irc` or `ircs`. TLS is enabled if is `ircs`. |
19 | | `server` | Address for the server. Eg: `irc.libera.chat`. |
20 | | `port` | Optional. Defaults to `6667` (if `irc`) or `6697` (if `ircs`). |
21 | | `channel` | Optional. List of channels, separated by a comma. |
22 |
23 | ### Examples
24 |
25 | Below is a few URL examples.
26 |
27 | - **Connect to Libera:**
28 | [ircs://irc.libera.chat](ircs://irc.libera.chat)
29 |
30 | - **Connect to Libera and join #halloy:**
31 | [ircs://irc.libera.chat/#halloy](ircs://irc.libera.chat/#halloy)
32 |
33 | - **Connect to OFTC on port 9999 and join #oftc and #asahi-dev:**
34 | [ircs://irc.oftc.net:9999/#oftc,#asahi-dev](ircs://irc.oftc.net:9999/#oftc,#asahi-dev)
35 |
36 | ## Halloy
37 |
38 | The `halloy://` scheme is used to import themes.
39 | The syntax for that is `halloy:///theme?e=base64EncodedThemeData`.
40 | A list of community created themes can be found [here](./configuration/themes/community.md).
41 |
--------------------------------------------------------------------------------
/book/theme/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/squidowl/halloy/c8e901d069a99afcc450e8b4a9b7f6aec6376c70/book/theme/favicon.png
--------------------------------------------------------------------------------
/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | #[cfg(windows)]
3 | {
4 | let _ = embed_resource::compile(
5 | "assets/windows/halloy.rc",
6 | embed_resource::NONE,
7 | );
8 | windows_exe_info::versioninfo::link_cargo_env();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/config.toml:
--------------------------------------------------------------------------------
1 | # Halloy config.
2 | #
3 | # For a complete list of available options,
4 | # please visit https://halloy.chat/configuration/
5 |
6 | [servers.liberachat]
7 | nickname = "__NICKNAME__"
8 | server = "irc.libera.chat"
9 | channels = ["#halloy"]
10 |
--------------------------------------------------------------------------------
/data/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "data"
3 | version.workspace = true
4 | authors.workspace = true
5 | license.workspace = true
6 | edition.workspace = true
7 |
8 | [features]
9 | dev = []
10 |
11 | [dependencies]
12 | thiserror = { workspace = true }
13 | futures = { workspace = true }
14 | tokio = { workspace = true, features = ["io-util", "fs"] }
15 | chrono = { workspace = true }
16 | bytes = { workspace = true }
17 | strum = { workspace = true }
18 | anyhow = { workspace = true }
19 | url = { workspace = true }
20 | tokio-stream = { workspace = true, features = ["time", "fs"] }
21 | timeago = { workspace = true }
22 | itertools = { workspace = true }
23 | emojis = { workspace = true }
24 | rand = { workspace = true }
25 | rand_chacha = { workspace = true }
26 | palette = { workspace = true }
27 | log = { workspace = true }
28 |
29 | base64 = "0.22.1"
30 | dirs-next = "2.0.0"
31 | xdg = "2.5.2"
32 | flate2 = "1.0"
33 | hex = "0.4.3"
34 | iced_core = "0.14.0-dev"
35 | seahash = "4.1.0"
36 | serde_json = "1.0"
37 | sha2 = "0.10.8"
38 | toml = "0.8.11"
39 | reqwest = { version = "0.12", features = ["json"] }
40 | fancy-regex = "0.14"
41 | walkdir = "2.5.0"
42 | nom = "7.1"
43 | const_format = "0.2.32"
44 | derive_more = { version = "2.0.1", features = ["full"] }
45 | image = "0.25.5"
46 | html-escape = "0.2.13"
47 |
48 | [dependencies.irc]
49 | path = "../irc"
50 |
51 | [dependencies.serde]
52 | version = "1.0"
53 | features = ["derive"]
54 |
55 | [lints]
56 | workspace = true
57 |
--------------------------------------------------------------------------------
/data/build.rs:
--------------------------------------------------------------------------------
1 | use std::path::Path;
2 | use std::process::Command;
3 |
4 | const VERSION: &str = include_str!("../VERSION");
5 |
6 | fn main() {
7 | let git_hash = Command::new("git")
8 | .args(["describe", "--always", "--dirty", "--exclude='*'"])
9 | .output()
10 | .ok()
11 | .filter(|output| output.status.success())
12 | .and_then(|x| String::from_utf8(x.stdout).ok());
13 |
14 | println!("cargo:rerun-if-changed=../VERSION");
15 | println!("cargo:rustc-env=VERSION={VERSION}");
16 |
17 | if let Some(hash) = git_hash.as_ref() {
18 | println!("cargo:rustc-env=GIT_HASH={hash}");
19 | }
20 |
21 | if git_hash.is_none() {
22 | return;
23 | }
24 |
25 | let Some(git_dir): Option = Command::new("git")
26 | .args(["rev-parse", "--git-dir"])
27 | .output()
28 | .ok()
29 | .filter(|output| output.status.success())
30 | .and_then(|x| String::from_utf8(x.stdout).ok())
31 | else {
32 | return;
33 | };
34 | // If heads starts pointing at something else (different branch)
35 | // we need to return
36 | let head = Path::new(&git_dir).join("HEAD");
37 | if head.exists() {
38 | println!("cargo:rerun-if-changed={}", head.display());
39 | }
40 | // if the thing head points to (branch) itself changes
41 | // we need to return
42 | let Some(head_ref): Option = Command::new("git")
43 | .args(["symbolic-ref", "HEAD"])
44 | .output()
45 | .ok()
46 | .filter(|output| output.status.success())
47 | .and_then(|x| String::from_utf8(x.stdout).ok())
48 | else {
49 | return;
50 | };
51 | let head_ref = Path::new(&git_dir).join(head_ref);
52 | if head_ref.exists() {
53 | println!("cargo:rerun-if-changed={}", head_ref.display());
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/data/src/appearance.rs:
--------------------------------------------------------------------------------
1 | pub use theme::Theme;
2 |
3 | pub mod theme;
4 |
5 | #[derive(Debug, Clone)]
6 | pub struct Appearance {
7 | pub selected: Selected,
8 | pub all: Vec,
9 | }
10 |
11 | impl Default for Appearance {
12 | fn default() -> Self {
13 | Self {
14 | selected: Selected::default(),
15 | all: vec![Theme::default()],
16 | }
17 | }
18 | }
19 |
20 | #[derive(Debug, Clone)]
21 | pub enum Selected {
22 | Static(Theme),
23 | Dynamic { light: Theme, dark: Theme },
24 | }
25 |
26 | impl Default for Selected {
27 | fn default() -> Self {
28 | Self::Static(Theme::default())
29 | }
30 | }
31 |
32 | impl Selected {
33 | pub fn is_dynamic(&self) -> bool {
34 | match self {
35 | Selected::Static(_) => false,
36 | Selected::Dynamic { .. } => true,
37 | }
38 | }
39 |
40 | pub fn dynamic(light: Theme, dark: Theme) -> Selected {
41 | Selected::Dynamic { light, dark }
42 | }
43 |
44 | pub fn specific(theme: Theme) -> Selected {
45 | Selected::Static(theme)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/data/src/audio.rs:
--------------------------------------------------------------------------------
1 | use std::fs::read;
2 | use std::path::PathBuf;
3 | use std::sync::Arc;
4 |
5 | use serde::Deserialize;
6 |
7 | use crate::Config;
8 |
9 | #[derive(Debug, Clone)]
10 | pub struct Sound(Arc>);
11 |
12 | impl AsRef<[u8]> for Sound {
13 | fn as_ref(&self) -> &[u8] {
14 | &self.0
15 | }
16 | }
17 |
18 | impl Sound {
19 | pub fn load(name: &str) -> Result {
20 | let source = if let Ok(internal) = Internal::try_from(name) {
21 | internal.bytes()
22 | } else {
23 | let sound_path = find_external_sound(name)?;
24 |
25 | read(sound_path)?
26 | };
27 |
28 | Ok(Sound(Arc::new(source)))
29 | }
30 | }
31 |
32 | #[derive(Debug, Clone, Deserialize)]
33 | #[serde(rename_all = "kebab-case")]
34 | pub enum Internal {
35 | Dong,
36 | Peck,
37 | Ring,
38 | Squeak,
39 | Whistle,
40 | Bonk,
41 | Sing,
42 | }
43 |
44 | impl Internal {
45 | pub fn bytes(&self) -> Vec {
46 | match self {
47 | Internal::Dong => include_bytes!("../../sounds/dong.ogg").to_vec(),
48 | Internal::Peck => include_bytes!("../../sounds/peck.ogg").to_vec(),
49 | Internal::Ring => include_bytes!("../../sounds/ring.ogg").to_vec(),
50 | Internal::Squeak => {
51 | include_bytes!("../../sounds/squeak.ogg").to_vec()
52 | }
53 | Internal::Whistle => {
54 | include_bytes!("../../sounds/whistle.ogg").to_vec()
55 | }
56 | Internal::Bonk => include_bytes!("../../sounds/bonk.ogg").to_vec(),
57 | Internal::Sing => include_bytes!("../../sounds/sing.ogg").to_vec(),
58 | }
59 | }
60 | }
61 |
62 | impl TryFrom<&str> for Internal {
63 | type Error = ();
64 |
65 | fn try_from(value: &str) -> Result {
66 | match value.to_lowercase().as_str() {
67 | "dong" => Ok(Self::Dong),
68 | "peck" => Ok(Self::Peck),
69 | "ring" => Ok(Self::Ring),
70 | "squeak" => Ok(Self::Squeak),
71 | "whistle" => Ok(Self::Whistle),
72 | "bonk" => Ok(Self::Bonk),
73 | "sing" => Ok(Self::Sing),
74 | _ => Err(()),
75 | }
76 | }
77 | }
78 |
79 | fn find_external_sound(sound: &str) -> Result {
80 | let sounds_dir = Config::sounds_dir();
81 |
82 | for e in walkdir::WalkDir::new(sounds_dir.clone())
83 | .into_iter()
84 | .filter_map(Result::ok)
85 | {
86 | if e.metadata().is_ok_and(|data| data.is_file())
87 | && e.file_name() == sound
88 | {
89 | return Ok(e.path().to_path_buf());
90 | }
91 | }
92 |
93 | let sounds_dir =
94 | if let Ok(sounds_dir) = sounds_dir.into_os_string().into_string() {
95 | format!(" in {sounds_dir}")
96 | } else {
97 | String::new()
98 | };
99 |
100 | Err(LoadError::NoSoundFound(sound.to_string(), sounds_dir))
101 | }
102 |
103 | #[derive(Debug, Clone, thiserror::Error)]
104 | pub enum LoadError {
105 | #[error(transparent)]
106 | File(Arc),
107 | #[error("sound \"{0}\" was not found{1}")]
108 | NoSoundFound(String, String),
109 | }
110 |
111 | impl From for LoadError {
112 | fn from(error: std::io::Error) -> Self {
113 | Self::File(Arc::new(error))
114 | }
115 | }
116 |
117 | #[derive(Debug, thiserror::Error)]
118 | pub enum InitializationError {
119 | #[error("unsupported")]
120 | Unsupported,
121 | }
122 |
--------------------------------------------------------------------------------
/data/src/channel.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::config;
4 |
5 | #[derive(Debug, Clone, Default, Deserialize, Serialize)]
6 | pub struct Settings {
7 | pub nicklist: Nicklist,
8 | pub topic: Topic,
9 | }
10 |
11 | impl From for Settings {
12 | fn from(config: config::buffer::Channel) -> Self {
13 | Self {
14 | nicklist: Nicklist::from(config.nicklist),
15 | topic: Topic::from(config.topic),
16 | }
17 | }
18 | }
19 |
20 | #[derive(Debug, Clone, Copy, Default, Deserialize)]
21 | #[serde(rename_all = "kebab-case")]
22 | pub enum Position {
23 | Left,
24 | #[default]
25 | Right,
26 | }
27 |
28 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)]
29 | pub struct Nicklist {
30 | pub enabled: bool,
31 | }
32 |
33 | impl From for Nicklist {
34 | fn from(config: config::buffer::channel::Nicklist) -> Self {
35 | Nicklist {
36 | enabled: config.enabled,
37 | }
38 | }
39 | }
40 |
41 | impl Default for Nicklist {
42 | fn default() -> Self {
43 | Self { enabled: true }
44 | }
45 | }
46 |
47 | impl Nicklist {
48 | pub fn toggle_visibility(&mut self) {
49 | self.enabled = !self.enabled;
50 | }
51 | }
52 |
53 | #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
54 | pub struct Topic {
55 | pub enabled: bool,
56 | }
57 |
58 | impl From for Topic {
59 | fn from(config: config::buffer::channel::Topic) -> Self {
60 | Topic {
61 | enabled: config.enabled,
62 | }
63 | }
64 | }
65 |
66 | impl Topic {
67 | pub fn toggle_visibility(&mut self) {
68 | self.enabled = !self.enabled;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/data/src/compression.rs:
--------------------------------------------------------------------------------
1 | use std::io;
2 | use std::io::prelude::*;
3 |
4 | use flate2::Compression;
5 | use flate2::read::GzDecoder;
6 | use flate2::write::GzEncoder;
7 | use serde::Serialize;
8 | use serde::de::DeserializeOwned;
9 |
10 | pub fn compress(value: &T) -> Result, Error> {
11 | let bytes = serde_json::to_vec(&value).map_err(Error::Encode)?;
12 | let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
13 | encoder.write_all(&bytes).map_err(Error::Compression)?;
14 | encoder.finish().map_err(Error::Compression)
15 | }
16 |
17 | pub fn decompress(data: &[u8]) -> Result {
18 | let mut bytes = vec![];
19 | let mut encoder = GzDecoder::new(data);
20 | encoder
21 | .read_to_end(&mut bytes)
22 | .map_err(Error::Decompression)?;
23 | serde_json::from_slice(&bytes).map_err(Error::Decode)
24 | }
25 |
26 | #[derive(Debug, thiserror::Error)]
27 | pub enum Error {
28 | #[error("compression failed")]
29 | Compression(io::Error),
30 | #[error("decompression failed")]
31 | Decompression(io::Error),
32 | #[error("encoding failed")]
33 | Encode(serde_json::Error),
34 | #[error("decoding failed")]
35 | Decode(serde_json::Error),
36 | }
37 |
--------------------------------------------------------------------------------
/data/src/config/actions.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | use crate::dashboard::{BufferAction, BufferFocusedAction};
4 |
5 | #[derive(Debug, Default, Clone, Deserialize)]
6 | pub struct Actions {
7 | #[serde(default)]
8 | pub sidebar: Sidebar,
9 | #[serde(default)]
10 | pub buffer: Buffer,
11 | }
12 |
13 | #[derive(Debug, Default, Clone, Deserialize)]
14 | pub struct Buffer {
15 | #[serde(default)]
16 | pub click_channel_name: BufferAction,
17 | #[serde(default)]
18 | pub click_highlight: BufferAction,
19 | #[serde(default)]
20 | pub click_username: BufferAction,
21 | #[serde(default)]
22 | pub local: BufferAction,
23 | #[serde(default)]
24 | pub message_channel: BufferAction,
25 | #[serde(default)]
26 | pub message_user: BufferAction,
27 | }
28 |
29 | #[derive(Debug, Default, Clone, Deserialize)]
30 | pub struct Sidebar {
31 | #[serde(default)]
32 | pub buffer: BufferAction,
33 | #[serde(default)]
34 | pub focused_buffer: Option,
35 | }
36 |
--------------------------------------------------------------------------------
/data/src/config/buffer/away.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Deserializer};
2 |
3 | #[derive(Debug, Clone, Copy, Default, Deserialize)]
4 | pub struct Away {
5 | #[serde(default)]
6 | pub appearance: Appearance,
7 | }
8 |
9 | impl Away {
10 | pub fn appearance(&self, is_user_away: bool) -> Option {
11 | is_user_away.then_some(self.appearance)
12 | }
13 | }
14 |
15 | #[derive(Debug, Clone, Copy, PartialEq)]
16 | pub enum Appearance {
17 | Dimmed(Option),
18 | Solid,
19 | }
20 |
21 | impl Default for Appearance {
22 | fn default() -> Self {
23 | Appearance::Dimmed(None)
24 | }
25 | }
26 |
27 | impl<'de> Deserialize<'de> for Appearance {
28 | fn deserialize(deserializer: D) -> Result
29 | where
30 | D: Deserializer<'de>,
31 | {
32 | #[derive(Deserialize)]
33 | #[serde(untagged)]
34 | enum AppearanceRepr {
35 | String(String),
36 | Struct(DimmedStruct),
37 | }
38 |
39 | #[derive(Deserialize)]
40 | struct DimmedStruct {
41 | dimmed: Option,
42 | }
43 |
44 | let repr = AppearanceRepr::deserialize(deserializer)?;
45 | match repr {
46 | AppearanceRepr::String(s) => match s.as_str() {
47 | "dimmed" => Ok(Appearance::Dimmed(None)),
48 | "solid" => Ok(Appearance::Solid),
49 | _ => Err(serde::de::Error::custom(format!(
50 | "unknown appearance: {s}",
51 | ))),
52 | },
53 | AppearanceRepr::Struct(s) => Ok(Appearance::Dimmed(s.dimmed)),
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/data/src/config/buffer/channel.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | use super::NicknameClickAction;
4 | use crate::buffer::Color;
5 | use crate::channel::Position;
6 | use crate::serde::default_bool_true;
7 |
8 | #[derive(Debug, Clone, Default, Deserialize)]
9 | pub struct Channel {
10 | #[serde(default)]
11 | pub nicklist: Nicklist,
12 | #[serde(default)]
13 | pub topic: Topic,
14 | #[serde(default)]
15 | pub message: Message,
16 | }
17 |
18 | #[derive(Debug, Clone, Default, Deserialize)]
19 | pub struct Message {
20 | #[serde(default)]
21 | pub nickname_color: Color,
22 | }
23 |
24 | #[derive(Debug, Clone, Deserialize)]
25 | pub struct Nicklist {
26 | #[serde(default = "default_bool_true")]
27 | pub enabled: bool,
28 | #[serde(default)]
29 | pub position: Position,
30 | #[serde(default)]
31 | pub color: Color,
32 | #[serde(default)]
33 | pub width: Option,
34 | #[serde(default)]
35 | pub alignment: Alignment,
36 | #[serde(default = "default_bool_true")]
37 | pub show_access_levels: bool,
38 | #[serde(default)]
39 | pub click: NicknameClickAction,
40 | }
41 |
42 | impl Default for Nicklist {
43 | fn default() -> Self {
44 | Self {
45 | enabled: default_bool_true(),
46 | position: Position::default(),
47 | color: Color::default(),
48 | width: Option::default(),
49 | alignment: Alignment::default(),
50 | show_access_levels: default_bool_true(),
51 | click: NicknameClickAction::default(),
52 | }
53 | }
54 | }
55 |
56 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
57 | #[serde(rename_all = "kebab-case")]
58 | pub enum Alignment {
59 | #[default]
60 | Left,
61 | Right,
62 | }
63 |
64 | #[derive(Debug, Clone, Copy, Deserialize)]
65 | pub struct Topic {
66 | #[serde(default)]
67 | pub enabled: bool,
68 | #[serde(default = "default_topic_banner_max_lines")]
69 | pub max_lines: u16,
70 | }
71 |
72 | impl Default for Topic {
73 | fn default() -> Self {
74 | Self {
75 | enabled: false,
76 | max_lines: default_topic_banner_max_lines(),
77 | }
78 | }
79 | }
80 |
81 | fn default_topic_banner_max_lines() -> u16 {
82 | 2
83 | }
84 |
--------------------------------------------------------------------------------
/data/src/config/ctcp.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | use crate::serde::default_bool_true;
4 |
5 | #[derive(Debug, Clone, Deserialize)]
6 | pub struct Ctcp {
7 | #[serde(default = "default_bool_true")]
8 | pub ping: bool,
9 | #[serde(default = "default_bool_true")]
10 | pub source: bool,
11 | #[serde(default = "default_bool_true")]
12 | pub time: bool,
13 | #[serde(default = "default_bool_true")]
14 | pub version: bool,
15 | }
16 |
17 | impl Default for Ctcp {
18 | fn default() -> Self {
19 | Self {
20 | ping: default_bool_true(),
21 | source: default_bool_true(),
22 | time: default_bool_true(),
23 | version: default_bool_true(),
24 | }
25 | }
26 | }
27 |
28 | impl Ctcp {
29 | pub fn client_info(&self) -> String {
30 | let mut commands = vec!["ACTION", "CLIENTINFO", "DCC"];
31 |
32 | if self.ping {
33 | commands.push("PING");
34 | }
35 |
36 | if self.source {
37 | commands.push("SOURCE");
38 | }
39 |
40 | if self.time {
41 | commands.push("TIME");
42 | }
43 |
44 | if self.version {
45 | commands.push("VERSION");
46 | }
47 |
48 | commands.join(" ")
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/data/src/config/file_transfer.rs:
--------------------------------------------------------------------------------
1 | use std::net::IpAddr;
2 | use std::num::NonZeroU16;
3 | use std::ops::RangeInclusive;
4 | use std::path::PathBuf;
5 |
6 | use serde::Deserialize;
7 |
8 | #[derive(Debug, Clone, Deserialize)]
9 | pub struct FileTransfer {
10 | /// Default directory to save files in. If not set, user will see a file dialog.
11 | #[serde(default)]
12 | pub save_directory: Option,
13 | /// If true, act as the "client" for the transfer. Requires the remote user act as the server.
14 | #[serde(default = "default_passive")]
15 | pub passive: bool,
16 | /// Time in seconds to wait before timing out a transfer waiting to be accepted.
17 | #[serde(default = "default_timeout")]
18 | pub timeout: u64,
19 | pub server: Option,
20 | }
21 |
22 | impl Default for FileTransfer {
23 | fn default() -> Self {
24 | Self {
25 | save_directory: None,
26 | passive: default_passive(),
27 | timeout: default_timeout(),
28 | server: None,
29 | }
30 | }
31 | }
32 |
33 | fn default_passive() -> bool {
34 | true
35 | }
36 |
37 | fn default_timeout() -> u64 {
38 | 60 * 5
39 | }
40 |
41 | #[derive(Debug, Clone)]
42 | pub struct Server {
43 | /// Address advertised to the remote user to connect to
44 | pub public_address: IpAddr,
45 | /// Address to bind to when accepting connections
46 | pub bind_address: IpAddr,
47 | /// Port range used to bind with
48 | pub bind_ports: RangeInclusive,
49 | }
50 |
51 | impl<'de> Deserialize<'de> for Server {
52 | fn deserialize(deserializer: D) -> Result
53 | where
54 | D: serde::Deserializer<'de>,
55 | {
56 | #[derive(Deserialize)]
57 | struct Data {
58 | public_address: IpAddr,
59 | bind_address: IpAddr,
60 | bind_port_first: NonZeroU16,
61 | bind_port_last: NonZeroU16,
62 | }
63 |
64 | let Data {
65 | public_address,
66 | bind_address,
67 | bind_port_first,
68 | bind_port_last,
69 | } = Data::deserialize(deserializer)?;
70 |
71 | if bind_port_last < bind_port_first {
72 | return Err(serde::de::Error::custom(
73 | "`bind_port_last` must be greater than or equal to `bind_port_first`",
74 | ));
75 | }
76 |
77 | Ok(Server {
78 | public_address,
79 | bind_address,
80 | bind_ports: bind_port_first.get()..=bind_port_last.get(),
81 | })
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/data/src/config/notification.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | use crate::audio::{self, Sound};
4 |
5 | pub type Loaded = Notification;
6 |
7 | #[derive(Debug, Clone, Deserialize)]
8 | pub struct Notification {
9 | #[serde(default)]
10 | pub show_toast: bool,
11 | pub sound: Option,
12 | pub delay: Option,
13 | #[serde(default)]
14 | pub exclude: Vec,
15 | #[serde(default)]
16 | pub include: Vec,
17 | }
18 |
19 | impl Default for Notification {
20 | fn default() -> Self {
21 | Self {
22 | show_toast: false,
23 | sound: None,
24 | delay: Some(500),
25 | exclude: Vec::default(),
26 | include: Vec::default(),
27 | }
28 | }
29 | }
30 |
31 | impl Notification {
32 | pub fn should_notify(&self, targets: Vec) -> bool {
33 | let is_target_filtered =
34 | |list: &Vec, targets: &Vec| -> bool {
35 | let wildcards = ["*", "all"];
36 |
37 | list.iter().any(|item| {
38 | wildcards.contains(&item.as_str()) || targets.contains(item)
39 | })
40 | };
41 | let target_included = is_target_filtered(&self.include, &targets);
42 | let target_excluded = is_target_filtered(&self.exclude, &targets);
43 |
44 | target_included || !target_excluded
45 | }
46 | }
47 |
48 | #[derive(Debug, Clone, Deserialize)]
49 | pub struct Notifications {
50 | #[serde(default)]
51 | pub connected: Notification,
52 | #[serde(default)]
53 | pub disconnected: Notification,
54 | #[serde(default)]
55 | pub reconnected: Notification,
56 | #[serde(default)]
57 | pub direct_message: Notification,
58 | #[serde(default)]
59 | pub highlight: Notification,
60 | #[serde(default)]
61 | pub file_transfer_request: Notification,
62 | #[serde(default)]
63 | pub monitored_online: Notification,
64 | #[serde(default)]
65 | pub monitored_offline: Notification,
66 | }
67 |
68 | impl Default for Notifications {
69 | fn default() -> Self {
70 | Self {
71 | connected: Notification::default(),
72 | disconnected: Notification::default(),
73 | reconnected: Notification::default(),
74 | direct_message: Notification::default(),
75 | highlight: Notification::default(),
76 | file_transfer_request: Notification::default(),
77 | monitored_online: Notification::default(),
78 | monitored_offline: Notification::default(),
79 | }
80 | }
81 | }
82 |
83 | impl Notifications {
84 | pub fn load_sounds(
85 | &self,
86 | ) -> Result, audio::LoadError> {
87 | let load = |notification: &Notification| -> Result<_, audio::LoadError> {
88 | Ok(Notification {
89 | show_toast: notification.show_toast,
90 | sound: notification.sound.as_deref().map(Sound::load).transpose()?,
91 | delay: notification.delay,
92 | exclude: notification.exclude.to_owned(),
93 | include: notification.include.to_owned(),
94 | })
95 | };
96 |
97 | Ok(Notifications {
98 | connected: load(&self.connected)?,
99 | disconnected: load(&self.disconnected)?,
100 | reconnected: load(&self.reconnected)?,
101 | direct_message: load(&self.direct_message)?,
102 | highlight: load(&self.highlight)?,
103 | file_transfer_request: load(&self.file_transfer_request)?,
104 | monitored_online: load(&self.monitored_online)?,
105 | monitored_offline: load(&self.monitored_offline)?,
106 | })
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/data/src/config/pane.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Debug, Clone, Deserialize, Default)]
4 | pub struct Pane {
5 | /// Default axis used when splitting a pane.
6 | #[serde(default)]
7 | pub split_axis: SplitAxis,
8 | }
9 |
10 | #[derive(Debug, Copy, Clone, Deserialize, Default)]
11 | #[serde(rename_all = "kebab-case")]
12 | pub enum SplitAxis {
13 | #[default]
14 | Horizontal,
15 | Vertical,
16 | }
17 |
--------------------------------------------------------------------------------
/data/src/config/proxy.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | #[derive(Debug, Clone, Deserialize)]
4 | #[serde(rename_all = "snake_case")]
5 | pub enum Proxy {
6 | Http {
7 | host: String,
8 | port: u16,
9 | username: Option,
10 | password: Option,
11 | },
12 | Socks5 {
13 | host: String,
14 | port: u16,
15 | username: Option,
16 | password: Option,
17 | },
18 | Tor,
19 | }
20 |
21 | impl From for irc::connection::Proxy {
22 | fn from(proxy: Proxy) -> irc::connection::Proxy {
23 | match proxy {
24 | Proxy::Http {
25 | host,
26 | port,
27 | username,
28 | password,
29 | } => irc::connection::Proxy::Http {
30 | host,
31 | port,
32 | username,
33 | password,
34 | },
35 | Proxy::Socks5 {
36 | host,
37 | port,
38 | username,
39 | password,
40 | } => irc::connection::Proxy::Socks5 {
41 | host,
42 | port,
43 | username,
44 | password,
45 | },
46 | Proxy::Tor => irc::connection::Proxy::Tor,
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/data/src/config/sidebar.rs:
--------------------------------------------------------------------------------
1 | use serde::Deserialize;
2 |
3 | use crate::serde::default_bool_true;
4 |
5 | #[derive(Debug, Copy, Clone, Deserialize)]
6 | pub struct Sidebar {
7 | #[serde(default)]
8 | pub max_width: Option,
9 | #[serde(default)]
10 | pub unread_indicator: UnreadIndicator,
11 | #[serde(default)]
12 | pub position: Position,
13 | #[serde(default = "default_bool_true")]
14 | pub show_user_menu: bool,
15 | }
16 |
17 | #[derive(Debug, Copy, Clone, Deserialize, Default)]
18 | #[serde(rename_all = "kebab-case")]
19 | pub enum UnreadIndicator {
20 | #[default]
21 | Dot,
22 | Title,
23 | None,
24 | }
25 |
26 | #[derive(Debug, Copy, Clone, Deserialize, Default)]
27 | #[serde(rename_all = "kebab-case")]
28 | pub enum Position {
29 | #[default]
30 | Left,
31 | Right,
32 | Top,
33 | Bottom,
34 | }
35 |
36 | impl Position {
37 | pub fn is_horizontal(&self) -> bool {
38 | match self {
39 | Position::Left | Position::Right => false,
40 | Position::Top | Position::Bottom => true,
41 | }
42 | }
43 | }
44 |
45 | impl Default for Sidebar {
46 | fn default() -> Self {
47 | Sidebar {
48 | max_width: None,
49 | unread_indicator: UnreadIndicator::default(),
50 | position: Position::default(),
51 | show_user_menu: default_bool_true(),
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/data/src/ctcp.rs:
--------------------------------------------------------------------------------
1 | use std::fmt;
2 |
3 | use irc::proto;
4 |
5 | // Reference: https://rawgit.com/DanielOaks/irc-rfcs/master/dist/draft-oakley-irc-ctcp-latest.html
6 |
7 | #[derive(Debug, Clone)]
8 | pub enum Command {
9 | Action,
10 | ClientInfo,
11 | DCC,
12 | Ping,
13 | Source,
14 | Version,
15 | Time,
16 | Unknown(String),
17 | }
18 |
19 | impl From<&str> for Command {
20 | fn from(command: &str) -> Self {
21 | match command.to_uppercase().as_ref() {
22 | "ACTION" => Command::Action,
23 | "CLIENTINFO" => Command::ClientInfo,
24 | "DCC" => Command::DCC,
25 | "PING" => Command::Ping,
26 | "SOURCE" => Command::Source,
27 | "VERSION" => Command::Version,
28 | "TIME" => Command::Time,
29 | _ => Command::Unknown(command.to_string()),
30 | }
31 | }
32 | }
33 |
34 | impl AsRef for Command {
35 | fn as_ref(&self) -> &str {
36 | match self {
37 | Command::Action => "ACTION",
38 | Command::ClientInfo => "CLIENTINFO",
39 | Command::DCC => "DCC",
40 | Command::Ping => "PING",
41 | Command::Source => "SOURCE",
42 | Command::Version => "VERSION",
43 | Command::Time => "TIME",
44 | Command::Unknown(command) => command.as_ref(),
45 | }
46 | }
47 | }
48 |
49 | #[derive(Debug)]
50 | pub struct Query<'a> {
51 | pub command: Command,
52 | pub params: Option<&'a str>,
53 | }
54 |
55 | pub fn is_query(text: &str) -> bool {
56 | text.starts_with('\u{1}')
57 | }
58 |
59 | pub fn parse_query(text: &str) -> Option {
60 | let query = text
61 | .strip_suffix('\u{1}')
62 | .unwrap_or(text)
63 | .strip_prefix('\u{1}')?;
64 |
65 | let (command, params) = if let Some((command, params)) =
66 | query.split_once(char::is_whitespace)
67 | {
68 | (command.to_uppercase(), Some(params))
69 | } else {
70 | (query.to_uppercase(), None)
71 | };
72 |
73 | let command = Command::from(command.as_str());
74 |
75 | Some(Query { command, params })
76 | }
77 |
78 | pub fn format(command: &Command, params: Option) -> String {
79 | let command = command.as_ref();
80 |
81 | if let Some(params) = params {
82 | format!("\u{1}{command} {params}\u{1}")
83 | } else {
84 | format!("\u{1}{command}\u{1}")
85 | }
86 | }
87 |
88 | pub fn query_command(
89 | command: &Command,
90 | target: String,
91 | params: Option,
92 | ) -> proto::Command {
93 | proto::Command::PRIVMSG(target, format(command, params))
94 | }
95 |
96 | pub fn query_message(
97 | command: &Command,
98 | target: String,
99 | params: Option,
100 | ) -> proto::Message {
101 | proto::command!("PRIVMSG", target, format(command, params))
102 | }
103 |
104 | pub fn response_message(
105 | command: &Command,
106 | target: String,
107 | params: Option,
108 | ) -> proto::Message {
109 | proto::command!("NOTICE", target, format(command, params))
110 | }
111 |
--------------------------------------------------------------------------------
/data/src/dashboard.rs:
--------------------------------------------------------------------------------
1 | use std::collections::HashMap;
2 | use std::io;
3 | use std::path::PathBuf;
4 |
5 | use serde::{Deserialize, Serialize};
6 |
7 | use crate::buffer::{self, Buffer};
8 | use crate::pane::Pane;
9 | use crate::serde::fail_as_none;
10 | use crate::{compression, environment};
11 |
12 | #[derive(Debug, Clone, Serialize, Deserialize)]
13 | pub struct Dashboard {
14 | pub pane: Pane,
15 | #[serde(default)]
16 | pub popout_panes: Vec,
17 | #[serde(default)]
18 | pub buffer_settings: BufferSettings,
19 | #[serde(default, deserialize_with = "fail_as_none")]
20 | pub focus_buffer: Option,
21 | }
22 |
23 | #[derive(Debug, Clone, Serialize, Deserialize, Default)]
24 | pub struct BufferSettings(HashMap);
25 |
26 | impl BufferSettings {
27 | pub fn get(&self, buffer: &buffer::Buffer) -> Option<&buffer::Settings> {
28 | self.0.get(&buffer.key())
29 | }
30 |
31 | pub fn entry(
32 | &mut self,
33 | buffer: &buffer::Buffer,
34 | maybe_default: Option,
35 | ) -> &mut buffer::Settings {
36 | self.0
37 | .entry(buffer.key())
38 | .or_insert_with(|| maybe_default.unwrap_or_default())
39 | }
40 | }
41 |
42 | #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
43 | #[serde(rename_all = "kebab-case")]
44 | pub enum BufferAction {
45 | #[default]
46 | NewPane,
47 | ReplacePane,
48 | NewWindow,
49 | }
50 |
51 | #[derive(Debug, Clone, Copy, Default, Deserialize, Serialize)]
52 | #[serde(rename_all = "kebab-case")]
53 | pub enum BufferFocusedAction {
54 | #[default]
55 | ClosePane,
56 | }
57 |
58 | impl Dashboard {
59 | pub fn load() -> Result {
60 | let path = path()?;
61 |
62 | let bytes = std::fs::read(path)?;
63 |
64 | Ok(compression::decompress(&bytes)?)
65 | }
66 |
67 | pub async fn save(self) -> Result<(), Error> {
68 | let path = path()?;
69 |
70 | let bytes = compression::compress(&self)?;
71 |
72 | tokio::fs::write(path, &bytes).await?;
73 |
74 | Ok(())
75 | }
76 | }
77 |
78 | fn path() -> Result {
79 | let parent = environment::data_dir();
80 |
81 | if !parent.exists() {
82 | std::fs::create_dir_all(&parent)?;
83 | }
84 |
85 | Ok(parent.join("dashboard.json.gz"))
86 | }
87 |
88 | #[derive(Debug, thiserror::Error)]
89 | pub enum Error {
90 | #[error(transparent)]
91 | Compression(#[from] compression::Error),
92 | #[error(transparent)]
93 | Io(#[from] io::Error),
94 | }
95 |
--------------------------------------------------------------------------------
/data/src/environment.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::path::PathBuf;
3 |
4 | pub const VERSION: &str = env!("VERSION");
5 | pub const GIT_HASH: Option<&str> = option_env!("GIT_HASH");
6 | pub const CONFIG_FILE_NAME: &str = "config.toml";
7 | pub const APPLICATION_ID: &str = "org.squidowl.halloy";
8 | pub const WIKI_WEBSITE: &str = "https://halloy.chat";
9 | pub const THEME_WEBSITE: &str = "https://themes.halloy.chat";
10 | pub const MIGRATION_WEBSITE: &str =
11 | "https://halloy.chat/guides/migrating-from-yaml.html";
12 | pub const RELEASE_WEBSITE: &str =
13 | "https://github.com/squidowl/halloy/releases/latest";
14 | pub const SOURCE_WEBSITE: &str = "https://github.com/squidowl/halloy/";
15 |
16 | pub fn formatted_version() -> String {
17 | let hash = GIT_HASH
18 | .map(|hash| format!(" ({hash})"))
19 | .unwrap_or_default();
20 |
21 | format!("{VERSION}{hash}")
22 | }
23 |
24 | pub fn config_dir() -> PathBuf {
25 | portable_dir().unwrap_or_else(platform_specific_config_dir)
26 | }
27 |
28 | pub fn data_dir() -> PathBuf {
29 | portable_dir().unwrap_or_else(|| {
30 | dirs_next::data_dir()
31 | .expect("expected valid data dir")
32 | .join("halloy")
33 | })
34 | }
35 |
36 | pub fn cache_dir() -> PathBuf {
37 | dirs_next::cache_dir()
38 | .expect("expected valid cache dir")
39 | .join("halloy")
40 | }
41 |
42 | /// Checks if a config file exists in the same directory as the executable.
43 | /// If so, it'll use that directory for both config & data dirs.
44 | fn portable_dir() -> Option {
45 | let exe = env::current_exe().ok()?;
46 | let dir = exe.parent()?;
47 |
48 | dir.join(CONFIG_FILE_NAME)
49 | .is_file()
50 | .then(|| dir.to_path_buf())
51 | }
52 |
53 | fn platform_specific_config_dir() -> PathBuf {
54 | #[cfg(target_os = "macos")]
55 | {
56 | xdg_config_dir().unwrap_or_else(|| {
57 | dirs_next::config_dir()
58 | .expect("expected valid config dir")
59 | .join("halloy")
60 | })
61 | }
62 | #[cfg(not(target_os = "macos"))]
63 | {
64 | dirs_next::config_dir()
65 | .expect("expected valid config dir")
66 | .join("halloy")
67 | }
68 | }
69 |
70 | #[cfg(target_os = "macos")]
71 | fn xdg_config_dir() -> Option {
72 | let config_dir = xdg::BaseDirectories::with_prefix("halloy")
73 | .ok()
74 | .and_then(|xdg| xdg.find_config_file(CONFIG_FILE_NAME))?;
75 |
76 | config_dir.parent().map(std::path::Path::to_path_buf)
77 | }
78 |
--------------------------------------------------------------------------------
/data/src/file_transfer.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 | use std::time::Duration;
3 |
4 | use chrono::{DateTime, Utc};
5 |
6 | pub use self::manager::Manager;
7 | pub use self::task::Task;
8 | use crate::user::Nick;
9 | use crate::{Server, dcc, server};
10 |
11 | pub mod manager;
12 | pub mod task;
13 |
14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15 | pub struct Id(u16);
16 |
17 | impl From for Id {
18 | fn from(value: u16) -> Self {
19 | Id(value)
20 | }
21 | }
22 |
23 | impl From for u16 {
24 | fn from(id: Id) -> Self {
25 | id.0
26 | }
27 | }
28 |
29 | #[derive(Debug, Clone, PartialEq, Eq)]
30 | pub struct FileTransfer {
31 | pub id: Id,
32 | pub server: Server,
33 | pub created_at: DateTime,
34 | pub direction: Direction,
35 | pub remote_user: Nick,
36 | pub filename: String,
37 | pub size: u64,
38 | pub status: Status,
39 | }
40 |
41 | impl FileTransfer {
42 | pub fn progress(&self) -> f64 {
43 | match self.status {
44 | Status::Active { transferred, .. } => {
45 | transferred as f64 / self.size as f64
46 | }
47 | Status::Completed { .. } => 1.0,
48 | _ => 0.0,
49 | }
50 | }
51 | }
52 |
53 | impl PartialOrd for FileTransfer {
54 | fn partial_cmp(&self, other: &Self) -> Option {
55 | Some(self.cmp(other))
56 | }
57 | }
58 |
59 | impl Ord for FileTransfer {
60 | fn cmp(&self, other: &Self) -> std::cmp::Ordering {
61 | self.created_at
62 | .cmp(&other.created_at)
63 | .reverse()
64 | .then_with(|| self.direction.cmp(&other.direction))
65 | .then_with(|| self.remote_user.cmp(&other.remote_user))
66 | .then_with(|| self.filename.cmp(&other.filename))
67 | }
68 | }
69 |
70 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
71 | pub enum Direction {
72 | Sent,
73 | Received,
74 | }
75 |
76 | #[derive(Debug, Clone, PartialEq, Eq)]
77 | pub enum Status {
78 | /// Pending approval
79 | PendingApproval,
80 | /// Pending reverse confirmation
81 | PendingReverseConfirmation,
82 | /// Queued (needs an open port to begin)
83 | Queued,
84 | /// Ready (waiting for remote user to connect)
85 | Ready,
86 | /// Transfer is actively sending / receiving
87 | Active { transferred: u64, elapsed: Duration },
88 | /// Transfer is complete
89 | Completed { elapsed: Duration, sha256: String },
90 | /// An error occurred
91 | Failed { error: String },
92 | }
93 |
94 | #[derive(Debug, Clone)]
95 | pub struct ReceiveRequest {
96 | pub from: Nick,
97 | pub dcc_send: dcc::Send,
98 | pub server: Server,
99 | pub server_handle: server::Handle,
100 | }
101 |
102 | #[derive(Debug)]
103 | pub struct SendRequest {
104 | pub to: Nick,
105 | pub path: PathBuf,
106 | pub server: Server,
107 | pub server_handle: server::Handle,
108 | }
109 |
--------------------------------------------------------------------------------
/data/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::large_enum_variant, clippy::too_many_arguments)]
2 |
3 | pub use self::appearance::Theme;
4 | pub use self::buffer::Buffer;
5 | pub use self::command::Command;
6 | pub use self::config::Config;
7 | pub use self::dashboard::Dashboard;
8 | pub use self::input::Input;
9 | pub use self::message::Message;
10 | pub use self::mode::Mode;
11 | pub use self::notification::Notification;
12 | pub use self::pane::Pane;
13 | pub use self::preview::Preview;
14 | pub use self::server::Server;
15 | pub use self::shortcut::Shortcut;
16 | pub use self::target::Target;
17 | pub use self::url::Url;
18 | pub use self::user::User;
19 | pub use self::version::Version;
20 | pub use self::window::Window;
21 |
22 | pub mod appearance;
23 | pub mod audio;
24 | pub mod buffer;
25 | pub mod channel;
26 | pub mod client;
27 | pub mod command;
28 | mod compression;
29 | pub mod config;
30 | pub mod ctcp;
31 | pub mod dashboard;
32 | pub mod dcc;
33 | pub mod environment;
34 | pub mod file_transfer;
35 | pub mod history;
36 | pub mod input;
37 | pub mod isupport;
38 | pub mod log;
39 | pub mod message;
40 | pub mod mode;
41 | pub mod notification;
42 | pub mod pane;
43 | pub mod preview;
44 | pub mod serde;
45 | pub mod server;
46 | pub mod shortcut;
47 | pub mod stream;
48 | pub mod target;
49 | pub mod time;
50 | pub mod url;
51 | pub mod user;
52 | pub mod version;
53 | pub mod window;
54 |
--------------------------------------------------------------------------------
/data/src/log.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 | use std::{fs, io};
3 |
4 | use chrono::{DateTime, Utc};
5 | use serde::{Deserialize, Serialize};
6 |
7 | use crate::environment;
8 |
9 | pub fn file() -> Result {
10 | let path = path()?;
11 |
12 | Ok(fs::OpenOptions::new()
13 | .write(true)
14 | .create(true)
15 | .append(false)
16 | .truncate(true)
17 | .open(path)?)
18 | }
19 |
20 | fn path() -> Result {
21 | let parent = environment::data_dir();
22 |
23 | if !parent.exists() {
24 | fs::create_dir_all(&parent)?;
25 | }
26 |
27 | Ok(parent.join("halloy.log"))
28 | }
29 |
30 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31 | pub struct Record {
32 | pub timestamp: DateTime,
33 | pub level: Level,
34 | pub message: String,
35 | }
36 |
37 | #[derive(
38 | Clone,
39 | Copy,
40 | PartialEq,
41 | Eq,
42 | PartialOrd,
43 | Ord,
44 | Debug,
45 | Hash,
46 | Serialize,
47 | Deserialize,
48 | strum::Display,
49 | )]
50 | #[strum(serialize_all = "UPPERCASE")]
51 | pub enum Level {
52 | Error,
53 | Warn,
54 | Info,
55 | Debug,
56 | Trace,
57 | }
58 |
59 | impl From for Level {
60 | fn from(level: log::Level) -> Self {
61 | match level {
62 | log::Level::Error => Level::Error,
63 | log::Level::Warn => Level::Warn,
64 | log::Level::Info => Level::Info,
65 | log::Level::Debug => Level::Debug,
66 | log::Level::Trace => Level::Trace,
67 | }
68 | }
69 | }
70 |
71 | #[derive(Debug, thiserror::Error)]
72 | pub enum Error {
73 | #[error(transparent)]
74 | Io(#[from] io::Error),
75 | #[error(transparent)]
76 | SetLog(#[from] log::SetLoggerError),
77 | #[error(transparent)]
78 | ParseLevel(#[from] log::ParseLevelError),
79 | }
80 |
--------------------------------------------------------------------------------
/data/src/message/source.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | pub use self::server::Server;
4 | use crate::User;
5 |
6 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7 | pub enum Source {
8 | User(User),
9 | Server(Option),
10 | Action(Option),
11 | Internal(Internal),
12 | }
13 |
14 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15 | pub enum Internal {
16 | Status(Status),
17 | Logs,
18 | }
19 |
20 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21 | pub enum Status {
22 | Success,
23 | Error,
24 | }
25 |
26 | pub mod server {
27 | #![allow(deprecated)]
28 | use serde::{Deserialize, Serialize};
29 |
30 | use crate::user::Nick;
31 |
32 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
33 | #[serde(untagged)]
34 | pub enum Server {
35 | #[deprecated(note = "use Server::Details")]
36 | Kind(Kind),
37 | Details(Details),
38 | }
39 |
40 | impl Server {
41 | pub fn new(kind: Kind, nick: Option) -> Self {
42 | Self::Details(Details { kind, nick })
43 | }
44 |
45 | pub fn kind(&self) -> Kind {
46 | match self {
47 | Server::Kind(kind) => *kind,
48 | Server::Details(details) => details.kind,
49 | }
50 | }
51 |
52 | pub fn nick(&self) -> Option<&Nick> {
53 | match self {
54 | Server::Kind(_) => None,
55 | Server::Details(details) => details.nick.as_ref(),
56 | }
57 | }
58 | }
59 |
60 | #[derive(
61 | Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
62 | )]
63 | #[serde(rename_all = "lowercase")]
64 | pub enum Kind {
65 | Join,
66 | Part,
67 | Quit,
68 | ReplyTopic,
69 | ChangeHost,
70 | MonitoredOnline,
71 | MonitoredOffline,
72 | StandardReply(StandardReply),
73 | Wallops,
74 | }
75 |
76 | #[derive(
77 | Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
78 | )]
79 | pub enum StandardReply {
80 | Fail,
81 | Warn,
82 | Note,
83 | }
84 |
85 | #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
86 | pub struct Details {
87 | pub kind: Kind,
88 | pub nick: Option,
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/data/src/notification.rs:
--------------------------------------------------------------------------------
1 | use crate::User;
2 | use crate::target::Channel;
3 | use crate::user::Nick;
4 |
5 | #[derive(Debug, PartialEq, Eq, Hash, Clone)]
6 | pub enum Notification {
7 | Connected,
8 | Disconnected,
9 | Reconnected,
10 | DirectMessage(User),
11 | Highlight { user: User, channel: Channel },
12 | FileTransferRequest(Nick),
13 | MonitoredOnline(Vec),
14 | MonitoredOffline(Vec),
15 | }
16 |
--------------------------------------------------------------------------------
/data/src/pane.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 |
3 | use crate::Buffer;
4 |
5 | #[derive(Debug, Clone, Deserialize, Serialize)]
6 | pub enum Pane {
7 | Split {
8 | axis: Axis,
9 | ratio: f32,
10 | a: Box,
11 | b: Box,
12 | },
13 | Buffer {
14 | buffer: Buffer,
15 | },
16 | Empty,
17 | }
18 |
19 | #[derive(Debug, Clone, Copy, Deserialize, Serialize)]
20 | pub enum Axis {
21 | Horizontal,
22 | Vertical,
23 | }
24 |
--------------------------------------------------------------------------------
/data/src/preview/cache.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use chrono::Utc;
4 | use serde::{Deserialize, Serialize};
5 | use tokio::fs;
6 | use url::Url;
7 |
8 | use super::{Preview, image};
9 | use crate::{config, environment};
10 |
11 | #[derive(Debug, Serialize, Deserialize)]
12 | #[serde(rename_all = "snake_case")]
13 | pub enum State {
14 | Ok(Preview),
15 | Error,
16 | }
17 |
18 | pub async fn load(url: &Url, config: &config::Preview) -> Option {
19 | let path = state_path(url);
20 |
21 | if !path.exists() {
22 | return None;
23 | }
24 |
25 | let state: State =
26 | serde_json::from_slice(&fs::read(&path).await.ok()?).ok()?;
27 |
28 | // Ensure the actual image is cached
29 | match &state {
30 | State::Ok(Preview::Card(card)) => {
31 | if !card.image.path.exists() {
32 | super::fetch(card.image.url.clone(), config).await.ok()?;
33 | }
34 | }
35 | State::Ok(Preview::Image(image)) => {
36 | if !image.path.exists() {
37 | super::fetch(image.url.clone(), config).await.ok()?;
38 | }
39 | }
40 | State::Error => {}
41 | }
42 |
43 | Some(state)
44 | }
45 |
46 | pub async fn save(url: &Url, state: State) {
47 | let path = state_path(url);
48 |
49 | if let Some(parent) = path.parent().filter(|p| !p.exists()) {
50 | let _ = fs::create_dir_all(parent).await;
51 | }
52 |
53 | let Ok(bytes) = serde_json::to_vec(&state) else {
54 | return;
55 | };
56 |
57 | let _ = fs::write(path, &bytes).await;
58 | }
59 |
60 | fn state_path(url: &Url) -> PathBuf {
61 | let hash =
62 | hex::encode(seahash::hash(url.as_str().as_bytes()).to_be_bytes());
63 |
64 | environment::cache_dir()
65 | .join("previews")
66 | .join("state")
67 | .join(&hash[..2])
68 | .join(&hash[2..4])
69 | .join(&hash[4..6])
70 | .join(format!("{hash}.json"))
71 | }
72 |
73 | pub(super) fn download_path(url: &Url) -> PathBuf {
74 | let hash = seahash::hash(url.as_str().as_bytes());
75 | // Unique download path so if 2 identical URLs are downloading
76 | // at the same time, they don't clobber eachother
77 | let nanos = Utc::now().timestamp_nanos_opt().unwrap_or_default();
78 |
79 | environment::cache_dir()
80 | .join("previews")
81 | .join("downloads")
82 | .join(format!("{hash}-{nanos}.part"))
83 | }
84 |
85 | pub(super) fn image_path(
86 | format: &image::Format,
87 | digest: &image::Digest,
88 | ) -> PathBuf {
89 | environment::cache_dir()
90 | .join("previews")
91 | .join("images")
92 | .join(&digest.as_ref()[..2])
93 | .join(&digest.as_ref()[2..4])
94 | .join(&digest.as_ref()[4..6])
95 | .join(format!(
96 | "{}.{}",
97 | digest.as_ref(),
98 | format.extensions_str()[0]
99 | ))
100 | }
101 |
--------------------------------------------------------------------------------
/data/src/preview/card.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use url::Url;
3 |
4 | use super::Image;
5 |
6 | #[derive(Debug, Clone, Serialize, Deserialize)]
7 | pub struct Card {
8 | pub url: Url,
9 | pub canonical_url: Url,
10 | pub image: Image,
11 | pub title: String,
12 | pub description: Option,
13 | }
14 |
--------------------------------------------------------------------------------
/data/src/preview/image.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 |
3 | use derive_more::derive::AsRef;
4 | use serde::{Deserialize, Serialize};
5 | use url::Url;
6 |
7 | use super::cache;
8 |
9 | pub type Format = image::ImageFormat;
10 | pub type Error = image::ImageError;
11 |
12 | /// SHA256 digest of image
13 | #[derive(Debug, Clone, Serialize, Deserialize, AsRef)]
14 | pub struct Digest(String);
15 |
16 | impl Digest {
17 | pub fn new(data: &[u8]) -> Self {
18 | Self(hex::encode(data))
19 | }
20 | }
21 |
22 | #[derive(Debug, Clone, Serialize, Deserialize)]
23 | pub struct Image {
24 | #[serde(with = "serde_format")]
25 | pub format: Format,
26 | pub url: Url,
27 | pub digest: Digest,
28 | pub path: PathBuf,
29 | }
30 |
31 | impl Image {
32 | pub fn new(format: Format, url: Url, digest: Digest) -> Self {
33 | let path = cache::image_path(&format, &digest);
34 |
35 | Self {
36 | format,
37 | url,
38 | digest,
39 | path,
40 | }
41 | }
42 | }
43 |
44 | pub fn format(bytes: &[u8]) -> Option {
45 | image::guess_format(bytes).ok()
46 | }
47 |
48 | mod serde_format {
49 | use serde::{Deserialize, Deserializer, Serialize, Serializer};
50 |
51 | use super::Format;
52 |
53 | pub fn serialize(
54 | format: &Format,
55 | serializer: S,
56 | ) -> Result {
57 | format.to_mime_type().serialize(serializer)
58 | }
59 |
60 | pub fn deserialize<'de, D: Deserializer<'de>>(
61 | deserializer: D,
62 | ) -> Result {
63 | let s = String::deserialize(deserializer)?;
64 |
65 | Format::from_mime_type(s)
66 | .ok_or(serde::de::Error::custom("invalid mime type"))
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/data/src/serde.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Deserializer};
2 |
3 | pub fn fail_as_none<'de, T, D>(deserializer: D) -> Result